【问题标题】:Javascript release resources automatically (like RAII)Javascript 自动释放资源(如 RAII)
【发布时间】:2012-07-26 11:53:39
【问题描述】:

我的一般问题是我可以使用哪些技术来确保在 Javascript 中清理/释放资源?目前,我正在采用 C(无 goto)方法在我的函数中查找返回或异常的每个执行路径,并确保进行清理。

我的具体示例是这样的:在 Node.js 中,我在对象成员函数中使用互斥锁(通过文件锁)(我需要互斥,因为我运行 Node.js 应用程序的多个实例并且在不同时有竞争条件实例与文件系统交互)。

例如,在 C++ 中,我会执行以下操作:

void MyClass::dangerous(void) {
     MyLock lock(&this->mutex);
     ...
     // at the end of this function, lock will be destructed and release this->mutex.
}

据我所知,JavaScript 不提供任何 RAII 功能。在 C 语言中,我会使用 gotos 在发生错误时展开我的资源分配,这样我就只有一个函数的返回路径。

有哪些技术可以在 Javascript 中实现类似的效果?

【问题讨论】:

    标签: javascript raii


    【解决方案1】:

    正如其他人可能已经指出的那样,您需要使用 try/finally。来自 c++ 的创建包装函数来模拟生命周期范围可能会更舒服。尝试在 javascript 控制台中运行以下代码以获取其用法示例:

    C++ 风格

    class MockFileIO {
        constructor(path) {
            console.log("Opening file stream to path", path);
            this.path = path;
        }
        destructor() {
            console.log("Closing file stream to path", this.path);
        }
        write(str) {
            console.log("Writing to file: ", str);
        }
    }
    
    async function run_with(resource, func) {
        try {
            func(resource);
        } catch(e) {
            throw e;
        } finally {
            resource.destructor();
        }
    }
    
    async function main() {
        console.log("Starting program");
        const fpath = "somewhere.txt";
        await run_with(new MockFileIO(fpath), (f) => {
            f.write("hello");
            f.write("world");
        });
        console.log("returning from main");
    }
    
    main();
    

    Golang 风格

    从那以后,我找到了一个更适合我个人使用 javascript 的范例。它基于 golang 的 defer 声明。您只需将代码包装在“范围”IIFE 中,当出于任何原因离开该函数时,延迟表达式将以相反的顺序执行,等待任何承诺。

    用法:

    scope(async (defer) => {
        const s = await openStream();
        defer(() => closeStream(s));
    
        const db = new DBConnection();
        defer(() => db.close());
    
        throw new Error("oh snap"); // could also be return
    
        // db.close() then closeStream(s)
    });
    

    作用域可以返回值并且是异步的。下面是一个使用 defer 技术编写的相同函数的示例:

    // without defer
    async function getUser() {
        const conn = new DB();
        const user = await conn.getUser();
        conn.close();
        return user;
    }
    // this is bad! conn.getUser could throw an error.
    

    变成:

    // with defer
    async function getUser() {
        return await scope(async defer => {
            const conn = new DB();
            defer(() => conn.close());
            return await conn.getUser();
        });
    }
    // conn.close is always called, even after error.
    

    基本上就是这样。范围也可以嵌套。定义范围的代码非常小:

    async function scope(fn) {
    
        const stack = [];
        const defer = (action) => {
            stack.push(action);
        };
        const errs = [];
    
        try {
            return await fn(defer);
        } catch(e) {
            errs.push(e);
        } finally {
            while (stack.length) {
                try {
                    await (stack.pop())();
                } catch(e) {
                    errs.push(e);
                }
            }
            for (const e of errs.slice(1)) {
                await error("error in deferred action: " + e);
            }
            if (errs.length) {
                throw errs[0]; // eslint-disable-line
            }
        }
    }
    

    scope 立即执行回调并将所有延迟函数收集到堆栈中。当函数退出时(通过返回或错误),延迟堆栈被弹出,直到所有延迟都被评估。延迟函数本身发生的任何错误都会被收集到一个错误列表中,当“范围”退出时会抛出第一个错误。我已经在我为工作编写的一个非常关键、容错率低的守护程序中使用了这种技术(实际上就是这段代码),它经受住了时间的考验。我希望这对遇到这种情况的人有所帮助。

    【讨论】:

      【解决方案2】:

      您可以使用闭包和 try ... finally 块来近似 RAII,如讨论:http://jeanlauliac.com/raii-in-javascript/

      例如,

      function using(start, close, execute) {
          try {
              start.call(this);
              return execute.call(this);
          } finally {
              close.call(this);
          }
      }
      
      // specialize to some resource (inside a Context2D wrapper)
          usingScaledLineWidth(execute) {
              const tmp = this.context.lineWidth;
              const start = () => {
                  this.context.lineWidth *= Math.abs(this.cf().a);
              };
              const close = () => {
                  this.context.lineWidth = tmp;
              };
              return using.call(this, start, close, execute);
          }
      
      
      // later RAII based usage
          stroke() {
              // have to manually do this because we're not scaling context
              if (this.context.strokeStyle !== "rgba(0, 0, 0, 0)") {
                  this.usingScaledLineWidth(()=>{
                      this.context.stroke();
                  });
              }
          }
      

      【讨论】:

        【解决方案3】:

        使用要在作用域结束时调用的回调列表。需要时给他们打电话。

        此方法用于例如取消初始化附加到浏览器窗口的附加处理程序。包含反初始化代码的回调存储在一个列表中,该列表在窗口的卸载事件上处理。

        不幸的是,由于异常安全要求,这种方法大多不适合范围管理。

        【讨论】:

          【解决方案4】:

          不幸的是,它没有语言声明(如析构函数和确定性破坏)。您必须为此使用try { ... } finally { ... } 语句。

          如果您想知道如何以类似于 C++ 的方式完成它,请查看https://github.com/cardinalby/ts-raii-scope,当我尝试使用 raii 容器和 TypeScript 装饰器时。但我不确定它是否适合生产代码,因为其他开发人员可能会对这种方法感到困惑。

          如果我们在谈论 TypeScript,我也认为它可以由 TypeScript transforms 实现

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2012-01-08
            • 2012-05-04
            • 2013-08-03
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多