16

我的一般问题是我可以使用哪些技术来确保在 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 中实现类似的效果?

4

4 回答 4

7

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

于 2020-01-07T19:23:40.270 回答
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();
            });
        }
    }
于 2019-03-02T06:25:03.120 回答
1

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

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

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

于 2012-07-30T02:37:32.493 回答
1

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

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

如果我们在谈论 TypeScript,我也认为它可以通过TypeScript 转换来实现

于 2019-03-07T16:22:54.957 回答