上一篇主要围绕Deno核心模块中安全沙箱机制与依赖引入管理进行了讲解。本篇文章我们将对Deno的执行机制进行一些介绍。
Deno的架构
先说说Deno的架构吧,这是官网的核心架构图
不过这张图很久没更新了,有些实现有些变化。主要变化就是libdeno已经替换成rusty_v8,js可以直接和rust进行交互了,使用rust就可以完成功能开发。
Deno里面js与rust的交互都会通过Deno.core.send和Deno.core.recv这两个方法,send发起同步任务,recv用来触发异步回调。
Tokio
Tokio是一个事件驱动的非阻塞I/O平台,用于使用Rust编程语言编写异步应用程序,相当于node中libuv。Tokio中的异步模型,是基于future实现的,熟悉Rust语言的应该很了解。基于我们熟悉的promise,简单对比一下,future的基本特征:
future特征:trait SimpleFuture { type Output; fn poll(&mut self, wake: fn()) -> Poll<Self::Output>;} enum Poll { Ready(T), Pending,}
js中的promise状态转移靠的是push,它本身就是一个事件源,能够主动去改变自身状态,我们在then函数中注册的回调,会自动在下一个微任务队列中执行;
而future靠的是poll,需要一个excutor去轮询获取其状态,也就是说excutor会在适当的时候执行future的poll方法,当返回状态为Poll::Pending也就意味需要excutor等待进行下一次轮询,当返回Poll::Ready也就是意味future已经完成,然后根据返回的值去判断是否出错,去执行下一步代码逻辑。
当然了,excutor不会不间断地去轮询,所以future的poll方法里面是可以设置一个waker(唤醒任务),去通知excutor什么时候该来轮询的。
Deno程序是怎么执行的
入口函数:cli/main.rs的main方法,主要干了两件事:
解析命令行参数然后创建对应的future(任务)
启动Tokio执行future(任务)
通过代码可以看出,关键的步骤就是Run对应的逻辑,也就是run_command方法。
核心逻辑就是Deno会创建一个MainWorker,就是我们的主进程,然后是四个关键步骤:执行用户逻辑代码->触发load事件->事件循环->触发unload事件
JS与Rust交互
代码路径:core/core.js
deno里面JS与Rust的交互只能通过send和recv这两个方法
send会根据opId去调用对应的rust方法,同步方法会直接返回,异步的话,需要通过recv去获取异步结果
Object.assign(window.Deno.core, { jsonOpAsync, jsonOpSync, setAsyncHandler,dispatch: send, ops, close,sharedQueue: {}, ... })
其中ops很关键,它是一个系统操作映射表,将操作和一个唯一的id进行关联,是JS和Rust之间沟通的桥梁。
整个core.js就是给Window.Deno.core定义这些api方法,Deno中触发任务的代码如下:
//同步Deno.core.dispatch(opId, scratchBytes, ...zeroCopy);//异步Deno.core.setAsyncHandler(opId,cb)Deno.core.dispatch(opId, scratchBytes, ...zeroCopy);
接下来,我们通过关键代码,看下基本流程是什么样的。
通过setAsyncHandler设定的回调函数,会进行初始化,回调函数会存储到全局的handles表里。初始化函数里设置recv的回调函数,供Tokio异步任务完成后触发。handleAsyncMsgFromRust函数则会从全局的回调队列中取出队首任务,根据opId去执行对应的handler。
核心代码实现:
最后来到core/bingding.rs的initialize_context,在这里是deno初始化核心方法的地方(都是挂在Deno.core这个对象下),send和recv也是在这里注入到JS中的。
用一张流程图,将上述的片段串一下:
总结
本期我们主要讲了deno的部分实现与执行机制,之后的文章我们会在deno的生态及工程化等方面继续进行讲解,敬请期待。