quickjs 封装 JavaScript 沙箱详情
1、场景
在前文JavaScript 沙箱探索 中声明了沙箱的接口,并且给出了一些简单的执行任意第三方 js 脚本的代码,但并未实现完整的 IJavaScriptShadowbox,下面便讲一下如何基于 quickjs 实现它。
quickjs 在 js 的封装库是quickjs-emscripten,基本原理是将 c 编译为 wasm 然后运行在浏览器、nodejs 上,它提供了以下基础的 api。
export interface LowLevelJavascriptVm<VmHandle> {
global: VmHandle;
undefined: VmHandle;
typeof(handle: VmHandle): string;
getNumber(handle: VmHandle): number;
getString(handle: VmHandle): string;
newNumber(value: number): VmHandle;
newString(value: string): VmHandle;
newObject(prototype?: VmHandle): VmHandle;
newFunction(
name: string,
value: VmFunctionImplementation<VmHandle>
): VmHandle;
getProp(handle: VmHandle, key: string | VmHandle): VmHandle;
setProp(handle: VmHandle, key: string | VmHandle, value: VmHandle): void;
defineProp(
handle: VmHandle,
key: string | VmHandle,
descriptor: VmPropertyDescriptor<VmHandle>
): void;
callFunction(
func: VmHandle,
thisVal: VmHandle,
...args: VmHandle[]
): VmCallResult<VmHandle>;
evalCode(code: string): VmCallResult<VmHandle>;
}
下面是一段官方的代码示例
import { getQuickJS } from "quickjs-emscripten";
async function main() {
const QuickJS = await getQuickJS();
const vm = QuickJS.createVm();
const world = vm.newString("world");
vm.setProp(vm.global, "NAME", world);
world.dispose();
const result = vm.evalCode(`"Hello " + NAME + "!"`);
if (result.error) {
console.log("Execution failed:", vm.dump(result.error));
result.error.dispose();
} else {
console.log("Success:", vm.dump(result.value));
result.value.dispose();
}
vm.dispose();
}
main();
可以看到,创建 vm 中的变量后还必须留意调用 dispose,有点像是后端连接数据库时必须注意关闭连接,而这其实是比较繁琐的,尤其是在复杂的情况下。简而言之,它的 api 太过于底层了。在 github issue 中有人创建了 quickjs-emscripten-sync,这给了吾辈很多灵感,所以吾辈基于quickjs-emscripten 封装了一些工具函数,辅助而非替代它。
2、简化底层 api
主要目的有两个:
- 自动调用
dispose - 提供更好的创建
vm值的方法
2.1自动调用 dispose
主要思路是自动收集所有需要调用 dispose 的值,使用高阶函数在 callback 执行完之后自动调用。
这里还需要注意避免不需要的多层嵌套代理,主要是考虑到下面更多的底层 api 基于它实现,而它们之间可能存在嵌套调用。
import { QuickJSHandle, QuickJSVm } from "quickjs-emscripten";
const QuickJSVmScopeSymbol = Symbol("QuickJSVmScope");
/**
* 为 QuickJSVm 添加局部作用域,局部作用域的所有方法调用不再需要手动释放内存
* @param vm
* @param handle
*/
export function withScope<F extends (vm: QuickJSVm) => any>(
vm: QuickJSVm,
handle: F
): {
value: ReturnType<F>;
dispose(): void;
} {
let disposes: (() => void)[] = [];
function wrap(handle: QuickJSHandle) {
disposes.push(() => handle.alive && handle.dispose());
return handle;
}
//避免多层代理
const isProxy = !!Reflect.get(vm, QuickJSVmScopeSymbol);
function dispose() {
if (isProxy) {
Reflect.get(vm, QuickJSVmScopeSymbol)();
return;
}
disposes.forEach((dispose) => dispose());
//手动释放闭包变量的内存
disposes.length = 0;
}
const value = handle(
isProxy
? vm
: new Proxy(vm, {
get(
target: QuickJSVm,
p: keyof QuickJSVm | typeof QuickJSVmScopeSymbol
): any {
if (p === QuickJSVmScopeSymbol) {
return dispose;
}
//锁定所有方法的 this 值为 QuickJSVm 对象而非 Proxy 对象
const res = Reflect.get(target, p, target);
if (
p.startsWith("new") ||
["getProp", "unwrapResult"].includes(p)
) {
return (...args: any[]): QuickJSHandle => {
return wrap(Reflect.apply(res, target, args));
};
}
if (["evalCode", "callFunction"].includes(p)) {
return (...args: any[]) => {
const res = (target[p] as any)(...args);
disposes.push(() => {
const handle = res.error ?? res.value;
handle.alive && handle.dispose();
});
return res;
};
}
if (typeof res === "function") {
return (...args: any[]) => {
return Reflect.apply(res, target, args);
};
}
return res;
},
})
);
return { value, dispose };
}
使用
withScope(vm, (vm) => {
const _hello = vm.newFunction("hello", () => {});
const _object = vm.newObject();
vm.setProp(_object, "hello", _hello);
vm.setProp(_object, "name", vm.newString("liuli"));
expect(vm.dump(vm.getProp(_object, "hello"))).not.toBeNull();
vm.setProp(vm.global, "VM_GLOBAL", _object);
}).dispose();
甚至支持嵌套调用,而且仅需要在最外层统一调用 dispose 即可
withScope(vm, (vm) =>
withScope(vm, (vm) => {
console.log(vm.dump(vm.unwrapResult(vm.evalCode("1+1"))));
})
).dispose();
2.2 提供更好的创建 vm 值的方法
主要思路是判断创建 vm 变量的类型,自动调用相应的函数,然后返回创建的变量。
import { QuickJSHandle, QuickJSVm } from "quickjs-emscripten";
import { withScope } from "./withScope";
type MarshalValue = { value: QuickJSHandle; dispose: () => void };
/**
* 简化使用 QuickJSVm 创建复杂对象的操作
* @param vm
*/
export function marshal(vm: QuickJSVm) {
function marshal(value: (...args: any[]) => any, name: string): MarshalValue;
function marshal(value: any): MarshalValue;
function marshal(value: any, name?: string): MarshalValue {
return withScope(vm, (vm) => {
function _f(value: any, name?: string): QuickJSHandle {
if (typeof value === "string") {
return vm.newString(value);
}
if (typeof value === "number") {
return vm.newNumber(value);
}
if (typeof value === "boolean") {
return vm.unwrapResult(vm.evalCode(`${value}`));
}
if (value === undefined) {
return vm.undefined;
}
if (value === null) {
return vm.null;
}
if (typeof value === "bigint") {
return vm.unwrapResult(vm.evalCode(`BigInt(${value})`));
}
if (typeof value === "function") {
return vm.newFunction(name!, value);
}
if (typeof value === "object") {
if (Array.isArray(value)) {
const _array = vm.newArray();
value.forEach((v) => {
if (typeof v === "function") {
throw new Error("数组中禁止包含函数,因为无法指定名字");
}
vm.callFunction(vm.getProp(_array, "push"), _array, _f(v));
});
return _array;
}
if (value instanceof Map) {
const _map = vm.unwrapResult(vm.evalCode("new Map()"));
value.forEach((v, k) => {
vm.unwrapResult(
vm.callFunction(vm.getProp(_map, "set"), _map, _f(k), _f(v, k))
);
});
return _map;
}
const _object = vm.newObject();
Object.entries(value).forEach(([k, v]) => {
vm.setProp(_object, k, _f(v, k));
});
return _object;
}
throw new Error("不支持的类型");
}
return _f(value, name);
});
}
return marshal;
}
使用
const mockHello = jest.fn();
const now = new Date();
const { value, dispose } = marshal(vm)({
name: "liuli",
age: 1,
sex: false,
hobby: [1, 2, 3],
account: {
username: "li",
},
hello: mockHello,
map: new Map().set(1, "a"),
date: now,
});
vm.setProp(vm.global, "vm_global", value);
dispose();
function evalCode(code: string) {
return vm.unwrapResult(vm.evalCode(code)).consume(vm.dump.bind(vm));
}
expect(evalCode("vm_global.name")).toBe("liuli");
expect(evalCode("vm_global.age")).toBe(1);
expect(evalCode("vm_global.sex")).toBe(false);
expect(evalCode("vm_global.hobby")).toEqual([1, 2, 3]);
expect(new Date(evalCode("vm_global.date"))).toEqual(now);
expect(evalCode("vm_global.account.username")).toEqual("li");
evalCode("vm_global.hello()");
expect(mockHello.mock.calls.length).toBe(1);
expect(evalCode("vm_global.map.size")).toBe(1);
expect(evalCode("vm_global.map.get(1)")).toBe("a");
目前支持的类型与 JavaScript 结构化克隆算法 对比,后者在很多地方(iframe/web worker/worker_threads)均有使用
| 对象类型 | quickjs | 结构化克隆 | 注意 |
|---|---|---|---|
| 所有的原始类型 | ? | ? | symbols 除外 |
| Function | ? | ? | |
| Array | ? | ? | |
| Object | ? | ? | 仅包括普通对象(如对象字面量) |
| Map | ? | ? | |
| Set | ? | ? | |
| Date | ? | ? | |
| Error | ? | ? | |
| Boolean | ? | ? | 对象 |
| String | ? | ? | 对象 |
| RegExp | ? | ? | lastIndex 字段不会被保留。 |
| Blob | ? | ? | |
| File | ? | ? | |
| FileList | ? | ? | |
| ArrayBuffer | ? | ? | |
| ArrayBufferView | ? | ? | 这基本上意味着所有的类型化数组 |
| ImageData | ? | ? |
以上不支持的非常见类型并非 quickjs 不支持,仅仅是 marshal 暂未支持。
3、实现 console/setTimeout/setInterval 等常见 api
由于 console/setTimeout/setInterval 均不是 js 语言级别的 api(但是浏览器、nodejs 均实现了),所以吾辈必须手动实现并注入它们。
3.1 实现 console
基本思路:为 vm 注入全局 console 对象,将参数 dump 之后转发到真正的 console api
import { QuickJSVm } from "quickjs-emscripten";
import { marshal } from "../util/marshal";
export interface IVmConsole {
log(...args: any[]): void;
info(...args: any[]): void;
warn(...args: any[]): void;
error(...args: any[]): void;
}
/**
* 定义 vm 中的 console api
* @param vm
* @param logger
*/
export function defineConsole(vm: QuickJSVm, logger: IVmConsole) {
const fields = ["log", "info", "warn", "error"] as const;
const dump = vm.dump.bind(vm);
const { value, dispose } = marshal(vm)(
fields.reduce((res, k) => {
res[k] = (...args: any[]) => {
logger[k](...args.map(dump));
};
return res;
}, {} as Record<string, Function>)
);
vm.setProp(vm.global, "console", value);
dispose();
}
export class BasicVmConsole implements IVmConsole {
error(...args: any[]): void {
console.error(...args);
}
info(...args: any[]): void {
console.info(...args);
}
log(...args: any[]): void {
console.log(...args);
}
warn(...args: any[]): void {
console.warn(...args);
}
}
使用
defineConsole(vm, new BasicVmConsole());
3.2 实现 setTimeout
基本思路:
基于 quickjs 实现 setTimeout 与 clearTimeout

为 vm 注入全局 setTimeout/clearTimeout 函数
setTimeout
- 将传过来的
callbackFunc注册为 vm 全局变量 - 在系统层执行
setTimeout - 将
clearTimeoutId => timeoutId写到 map,返回一个clearTimeoutId
- 执行刚刚注册的全局 vm 变量,并清除回调
clearTimeout: 根据 clearTimeoutId 在系统层调用真实的 clearTimeout
不直接返回 setTimeout 返回值的原因在于在 nodejs 中返回值是一个对象而非一个数字,所以需要使用 map 兼容
import { QuickJSVm } from "quickjs-emscripten";
import { withScope } from "../util/withScope";
import { VmSetInterval } from "./defineSetInterval";
import { deleteKey } from "../util/deleteKey";
import { CallbackIdGenerator } from "@webos/ipc-main";
/**
* 注入 setTimeout 方法
* 需要在注入后调用 {@link defineEventLoop} 让 vm 的事件循环跑起来
* @param vm
*/
export function defineSetTimeout(vm: QuickJSVm): VmSetInterval {
const callbackMap = new Map<string, any>();
function clear(id: string) {
withScope(vm, (vm) => {
deleteKey(
vm,
vm.unwrapResult(vm.evalCode(`VM_GLOBAL.setTimeoutCallback`)),
id
);
}).dispose();
clearInterval(callbackMap.get(id));
callbackMap.delete(id);
}
withScope(vm, (vm) => {
const vmGlobal = vm.getProp(vm.global, "VM_GLOBAL");
if (vm.typeof(vmGlobal) === "undefined") {
throw new Error("VM_GLOBAL 不存在,需要先执行 defineVmGlobal");
}
vm.setProp(vmGlobal, "setTimeoutCallback", vm.newObject());
vm.setProp(
vm.global,
"setTimeout",
vm.newFunction("setTimeout", (callback, ms) => {
const id = CallbackIdGenerator.generate();
//此处已经是异步了,必须再包一层
withScope(vm, (vm) => {
const callbacks = vm.unwrapResult(
vm.evalCode("VM_GLOBAL.setTimeoutCallback")
);
vm.setProp(callbacks, id, callback);
//此处还是异步的,必须再包一层
const timeout = setTimeout(
() =>
withScope(vm, (vm) => {
const callbacks = vm.unwrapResult(
vm.evalCode(`VM_GLOBAL.setTimeoutCallback`)
);
const callback = vm.getProp(callbacks, id);
vm.callFunction(callback, vm.null);
callbackMap.delete(id);
}).dispose(),
vm.dump(ms)
);
callbackMap.set(id, timeout);
}).dispose();
return vm.newString(id);
})
);
vm.setProp(
vm.global,
"clearTimeout",
vm.newFunction("clearTimeout", (id) => clear(vm.dump(id)))
);
}).dispose();
return {
callbackMap,
clear() {
[...callbackMap.keys()].forEach(clear);
},
};
}
使用
const vmSetTimeout = defineSetTimeout(vm);
withScope(vm, (vm) => {
vm.evalCode(`
const begin = Date.now()
setInterval(() => {
console.log(Date.now() - begin)
}, 100)
`);
}).dispose();
vmSetTimeout.clear();
3.3 实现 setInterval
基本上,与实现 setTimeout 流程差不多
import { QuickJSVm } from "quickjs-emscripten";
import { withScope } from "../util/withScope";
import { deleteKey } from "../util/deleteKey";
import { CallbackIdGenerator } from "@webos/ipc-main";
export interface VmSetInterval {
callbackMap: Map<string, any>;
clear(): void;
}
/**
* 注入 setInterval 方法
* 需要在注入后调用 {@link defineEventLoop} 让 vm 的事件循环跑起来
* @param vm
*/
export function defineSetInterval(vm: QuickJSVm): VmSetInterval {
const callbackMap = new Map<string, any>();
function clear(id: string) {
withScope(vm, (vm) => {
deleteKey(
vm,
vm.unwrapResult(vm.evalCode(`VM_GLOBAL.setTimeoutCallback`)),
id
);
}).dispose();
clearInterval(callbackMap.get(id));
callbackMap.delete(id);
}
withScope(vm, (vm) => {
const vmGlobal = vm.getProp(vm.global, "VM_GLOBAL");
if (vm.typeof(vmGlobal) === "undefined") {
throw new Error("VM_GLOBAL 不存在,需要先执行 defineVmGlobal");
}
vm.setProp(vmGlobal, "setIntervalCallback", vm.newObject());
vm.setProp(
vm.global,
"setInterval",
vm.newFunction("setInterval", (callback, ms) => {
const id = CallbackIdGenerator.generate();
//此处已经是异步了,必须再包一层
withScope(vm, (vm) => {
const callbacks = vm.unwrapResult(
vm.evalCode("VM_GLOBAL.setIntervalCallback")
);
vm.setProp(callbacks, id, callback);
const interval = setInterval(() => {
withScope(vm, (vm) => {
vm.callFunction(
vm.unwrapResult(
vm.evalCode(`VM_GLOBAL.setIntervalCallback['${id}']`)
),
vm.null
);
}).dispose();
}, vm.dump(ms));
callbackMap.set(id, interval);
}).dispose();
return vm.newString(id);
})
);
vm.setProp(
vm.global,
"clearInterval",
vm.newFunction("clearInterval", (id) => clear(vm.dump(id)))
);
}).dispose();
return {
callbackMap,
clear() {
[...callbackMap.keys()].forEach(clear);
},
};
}
3.4 实现事件循环
但有一点麻烦的是,quickjs-emscripten 不会自动执行事件循环,即 Promise 在 resolve 之后不会自动执行下一步。官方提供了 executePendingJobs 方法让我们手动执行事件循环,如下所示
const { log } = defineMockConsole(vm);
withScope(vm, (vm) => {
vm.evalCode(`Promise.resolve().then(()=>console.log(1))`);
}).dispose();
expect(log.mock.calls.length).toBe(0);
vm.executePendingJobs();
expect(log.mock.calls.length).toBe(1);
所以我们实现可以使用一个自动调用 executePendingJobs 的函数
import { QuickJSVm } from "quickjs-emscripten";
export interface VmEventLoop {
clear(): void;
}
/**
* 定义 vm 中的事件循环机制,尝试循环执行等待的异步操作
* @param vm
*/
export function defineEventLoop(vm: QuickJSVm) {
const interval = setInterval(() => {
vm.executePendingJobs();
}, 100);
return {
clear() {
clearInterval(interval);
},
};
}
现在只要调用 defineEventLoop 即会循环执行 executePendingJobs 函数了
const { log } = defineMockConsole(vm);
const eventLoop = defineEventLoop(vm);
try {
withScope(vm, (vm) => {
vm.evalCode(`Promise.resolve().then(()=>console.log(1))`);
}).dispose();
expect(log.mock.calls.length).toBe(0);
await wait(100);
expect(log.mock.calls.length).toBe(1);
} finally {
eventLoop.clear();
}
4、实现沙箱与系统之间的通信
现在,我们沙箱还欠缺的就是通信机制了,下面我们便实现一个 EventEmiiter。
核心是让系统层和沙箱都实现 EventEmitter,quickjs 允许我们向沙箱中注入方法,所以我们可以注入一个 Map 和 emitMain 函数。让沙箱既能够向 Map 中注册事件以供系统层调用,也能通过 emitMain 向系统层发送事件。
沙箱与系统之间的通信:

import { QuickJSHandle, QuickJSVm } from "quickjs-emscripten";
import { marshal } from "../util/marshal";
import { withScope } from "../util/withScope";
import { IEventEmitter } from "@webos/ipc-main";
export type VmMessageChannel = IEventEmitter & {
listenerMap: Map<string, ((msg: any) => void)[]>;
};
/**
* 定义消息通信
* @param vm
*/
export function defineMessageChannel(vm: QuickJSVm): VmMessageChannel {
const res = withScope(vm, (vm) => {
const vmGlobal = vm.getProp(vm.global, "VM_GLOBAL");
if (vm.typeof(vmGlobal) === "undefined") {
throw new Error("VM_GLOBAL 不存在,需要先执行 defineVmGlobal");
}
const listenerMap = new Map<string, ((msg: string) => void)[]>();
const messagePort = marshal(vm)({
//region vm 进程回调函数定义
listenerMap: new Map(),
//给 vm 进程用的
emitMain(channel: QuickJSHandle, msg: QuickJSHandle) {
const key = vm.dump(channel);
const value = vm.dump(msg);
if (!listenerMap.has(key)) {
console.log("主进程没有监听 api: ", key, value);
return;
}
listenerMap.get(key)!.forEach((fn) => {
try {
fn(value);
} catch (e) {
console.error("执行回调函数发生错误: ", e);
}
});
},
//endregion
});
vm.setProp(vmGlobal, "MessagePort", messagePort.value);
//给主进程用的
function emitVM(channel: string, msg: string) {
withScope(vm, (vm) => {
const _map = vm.unwrapResult(
vm.evalCode("VM_GLOBAL.MessagePort.listenerMap")
);
const _get = vm.getProp(_map, "get");
const _array = vm.unwrapResult(
vm.callFunction(_get, _map, vm.newString(channel))
);
if (!vm.dump(_array)) {
return;
}
for (
let i = 0, length = vm.dump(vm.getProp(_array, "length"));
i < length;
i++
) {
vm.callFunction(
vm.getProp(_array, vm.newNumber(i)),
vm.null,
marshal(vm)(msg).value
);
}
}).dispose();
}
return {
emit: emitVM,
offByChannel(channel: string): void {
listenerMap.delete(channel);
},
on(channel: string, handle: (data: any) => void): void {
if (!listenerMap.has(channel)) {
listenerMap.set(channel, []);
}
listenerMap.get(channel)!.push(handle);
},
listenerMap,
} as VmMessageChannel;
});
res.dispose();
return res.value;
}
可以看到,我们除了实现了 IEventEmitter,还额外添加了字段 listenerMap,这主要是希望向上层暴露更多细节,便于在需要的时候(例如清理全部注册的事件)可以直接实现。
使用
defineVmGlobal(vm);
const messageChannel = defineMessageChannel(vm);
const mockFn = jest.fn();
messageChannel.on("hello", mockFn);
withScope(vm, (vm) => {
vm.evalCode(`
class QuickJSEventEmitter {
emit(channel, data) {
VM_GLOBAL.MessagePort.emitMain(channel, data);
}
on(channel, handle) {
if (!VM_GLOBAL.MessagePort.listenerMap.has(channel)) {
VM_GLOBAL.MessagePort.listenerMap.set(channel, []);
}
VM_GLOBAL.MessagePort.listenerMap.get(channel).push(handle);
}
offByChannel(channel) {
VM_GLOBAL.MessagePort.listenerMap.delete(channel);
}
}
const em = new QuickJSEventEmitter()
em.emit('hello', 'liuli')
`);
}).dispose();
expect(mockFn.mock.calls[0][0]).toBe("liuli");
messageChannel.listenerMap.clear();
5、实现 IJavaScriptShadowbox
最终,我们以上实现的功能集合起来,便实现了 IJavaScriptShadowbox
import { IJavaScriptShadowbox } from "./IJavaScriptShadowbox";
import { getQuickJS, QuickJS, QuickJSVm } from "quickjs-emscripten";
import {
BasicVmConsole,
defineConsole,
defineEventLoop,
defineMessageChannel,
defineSetInterval,
defineSetTimeout,
defineVmGlobal,
VmEventLoop,
VmMessageChannel,
VmSetInterval,
withScope,
} from "@webos/quickjs-emscripten-utils";
export class QuickJSShadowbox implements IJavaScriptShadowbox {
private vmMessageChannel: VmMessageChannel;
private vmEventLoop: VmEventLoop;
private vmSetInterval: VmSetInterval;
private vmSetTimeout: VmSetInterval;
private constructor(readonly vm: QuickJSVm) {
defineConsole(vm, new BasicVmConsole());
defineVmGlobal(vm);
this.vmSetTimeout = defineSetTimeout(vm);
this.vmSetInterval = defineSetInterval(vm);
this.vmEventLoop = defineEventLoop(vm);
this.vmMessageChannel = defineMessageChannel(vm);
}
destroy(): void {
this.vmMessageChannel.listenerMap.clear();
this.vmEventLoop.clear();
this.vmSetInterval.clear();
this.vmSetTimeout.clear();
this.vm.dispose();
}
eval(code: string): void {
withScope(this.vm, (vm) => {
vm.unwrapResult(vm.evalCode(code));
}).dispose();
}
emit(channel: string, data?: any): void {
this.vmMessageChannel.emit(channel, data);
}
on(channel: string, handle: (data: any) => void): void {
this.vmMessageChannel.on(channel, handle);
}
offByChannel(channel: string) {
this.vmMessageChannel.offByChannel(channel);
}
private static quickJS: QuickJS;
static async create() {
if (!QuickJSShadowbox.quickJS) {
QuickJSShadowbox.quickJS = await getQuickJS();
}
return new QuickJSShadowbox(QuickJSShadowbox.quickJS.createVm());
}
static destroy() {
QuickJSShadowbox.quickJS = null as any;
}
}
在系统层使用
const shadowbox = await QuickJSShadowbox.create();
const mockConsole = defineMockConsole(shadowbox.vm);
shadowbox.eval(code);
shadowbox.emit(AppChannelEnum.Open);
expect(mockConsole.log.mock.calls[0][0]).toBe("open");
shadowbox.emit(WindowChannelEnum.AllClose);
expect(mockConsole.log.mock.calls[1][0]).toBe("all close");
shadowbox.destroy();
在沙箱使用
const eventEmitter = new QuickJSEventEmitter();
eventEmitter.on(AppChannelEnum.Open, async () => {
console.log("open");
});
eventEmitter.on(WindowChannelEnum.AllClose, async () => {
console.log("all close");
});
6、目前 quickjs 沙箱的限制
下面是目前实现的一些限制,也是以后可以继续改进的点
console 仅支持常见的 log/info/warn/error 方法
setTimeout/setInterval 事件循环时间没有保证,目前大约在 100ms 调用一次
无法使用 chrome devtool 调试,也不会处理 sourcemap(figma 至今的开发体验仍然如此,后面可能添加开关支持在 web worker 中调试)
vm 中出现错误不会将错误抛出来并打印在控制台
各个 api 调用的顺序与清理顺序必须手动保证是相反的,例如 vm 创建必须在 defineSetTimeout 之前,而 defineSetTimeout 的清理函数调用必须在 vm.dispose 之前
不能在 messageChannel.on 回调中同步调用 vm.dispose,因为是同步调用的






