IO
冯诺依曼机器的工作模式,是 CPU 和 内存作为数据计算的核心,IO 设备是数据输入输出的外围设备。其基本工作模式是:
- 从 IO 设备中的输入通道读取数据到内存
- CPU 处理内存中的数据
- 将处理后的数据写到 IO 设备中的输出通道
这些工作由操作系统来控制协调,但上层的应用程序也是基于类似的工作流程对数据进行处理,只不过上层的程序不会直接操作硬件,而是通过操作系统和应用程序标准库提供的程序接口进行操作。
根据 C 语言生态的程序特性,一个程序往往可以被抽象成一个 main 函数,main 函数接受两个参数,分别是 int 类型的 argc 和 char** 类型的 argv,并有一个 int 类型的返回值。main 函数的返回值表示程序执行的结果,argc 表示命令行参数的数量,argv 是一个字符串数组,表示命令行参数的值。这个输入输出的接口,是 C 语言程序的标准接口,也是操作系统和应用程序标准库提供的接口,被称为程序的标准输入输出接口。
一个程序除了标准输入和标准输出,往往需要处理其他的 IO 设备,和多个 IO 设备进行交互。操作系统和应用程序标准库需要提供一套接口,让上层程序可以方便地进行 IO 调用。
IO 接口
一个程序有标准输入输出接口,还有其他 IO 接口。一个程序如果只是对于标准输入输出进行操作,那么这个程序一般就是一个控制台程序,但是现代的程序,往往是一个 GUI 程序或者 Web 服务器,需要和多个 IO 设备进行交互,比如磁盘、网卡、显示器、鼠标键盘等。程序的数据流,往往是从一个 IO 设备中持续读取数据,经过处理,再写入到另一个 IO 设备中。此时,程序的不止一个标准流,而是多条流。
典型的流有:
- 标准 IO 流
- UI 交互流 (键盘鼠标 -> 显示器流)
- socket 网络流
- 磁盘文件流
前端程序中的 IO 流
- 键盘鼠标->显示器流
- 网络流
- 文件流
其中,前端框架帮助我们处理了键盘鼠标->显示器流,极大地简化了前端管理页面的难度。
声明式 IO
声明式 IO,程序员无需进行具体的 IO 操作,只需要声明需要从外界获得一个怎样的数据,并且声明一定的描述信息,框架会自动处理 IO 操作,将数据返回给程序员。由于 IO 操作往往是命令式的,而其是异步的,进行这些操作是非常不纯的操作,在函数式编程中,我们需要采取特定的手段来处理这些不纯的操作。
响应式编程和数据驱动 IO
IO 就是 state,state 就是 IO。使用数据驱动 IO 就是将 IO 操作转化为一个 state 修改的操作,这样,在编码的角度上来看,我们就可以使用统一的编码模型去管理 IO 和 state。
具体地,将一个 IO 行为与一个状态关联起来,当状态改变时,IO 行为自动执行,是响应式编程的一个子集。本质上 IO 操作其实就是一个异步的外界数据和状态,我们需要将外界的数据获取到当前的程序内存中进行暂存,而在编程语言中,内存中的数据往往由该语言的数据类型和变量构成。
function getUser(id) {
return fetch('/users/' + id).then((res)=> {
const body = await res.body()
return JSON.parse(await body.json())
})
}在上述的例子中,在内存中有一个变量 id,它映射成为一个 User 对象,只要 id 发生了变化,那么 User 对象也会重新获取。只要将 id 做成响应式对象,并让 getUser 订阅 id 的变化,然后自己执行,便可以拿到新的 User 值。
另外,对于一个数据驱动的 IO 系统,依然需要考虑普通的 IO 请求,它有什么需求和操作,例如:
- 生命周期管理,数据不是全部一起加载进来的,也不是一直驻留在内存中,使用懒加载技术减少服务器的压力;
- 幂等性和可撤销,一个接口相同的调用得到相同的结果;一般地,GET 是幂等的,PUT 和 DELETE 大概率是,POST 不是;同样地,在内存中地操作中,get,set,delete 一个变量的值往往是幂等的,而 add 和 update 往往不是;
- 回滚和事务,当一次请求多个接口时,如果需要保证多个接口同时完成,或者同时失败;
- 故障重试;
- 缓存、限流、防抖;