初识cycle.js(WIP)

cycle.js是一个函数式、响应式的前端框架。本文将对cycle.js的基本概念进行讲解,其大部分来源于官方文档。

本文概念主要来自于cycle.js官方指南

文章当前状态

  1. 图片待补全
  2. 文本待修订

简介

cycle.js是一个函数式、响应式的前端框架。

概念

cycle.js的概念对于不熟悉js、函数式、响应式的程序员而言,学习门槛比较高或者学习曲线比较陡峭。

其中,熟悉js意味着你知道js发展至今的大部分缺陷以及对应弥补方法,知道js的原型链,知道这个原型链可以怎么模拟各种编程范式,然后这些编程范式又是讲的什么有何优劣;熟悉函数式意味着你有学过haskell、scala、idris等等函数式语言,知道其无副作用,懒执行等等特性;熟悉响应式意味着你知道pull/push、Proactive/Passive、Listenable/Reactive等等概念。

不知道也没关系,反正此系列文章中会把要用到的都讲到,毕竟我就是从小白这样一路采坑过来的成了老白的。

人机交互模型

先讲讲人机交互模型,HCI。人机交互模型是一个双向过程:两边同时在speak和listen。
GUI中,计算机通过屏幕来speak,人通过屏幕来listen。不同交互模式中,计算机通过不同的设备告知给人的不同知觉。而另一面,人通过键盘、鼠标或者触屏来speak。
人机交互模型就像是一个对话:两方都在通过不停的speak和listen来交换信息。

Sense/Actuator 与 IO

有种思想是把计算机当成是输入输出设备的集合,输出设备将信息呈现给人,而输入设备检测人输入的信息。
而人则是看成感知器官和执行器官的集合,分别和输入输出设备连接。

输入输出设备和计算机的关系,稍稍抽象一下就成了这样的函数。这里面的输入输出设备到底是什么我没还不知道,但是这个抽象没毛病。
同理人也可以被抽象成一个类似的函数。
仔细想想人和计算机的模式不是很像么。
稍稍转换一下,就成了这样。
看上去是不是很对称,一者的sense和另一者的actuator连接。
也可以转换成计算机的模式。

疑问

虽然这个抽象看上去很合理,但是还有比较现实的问题存在

  1. 具体sense和actuator的类型是什么
  2. 什么时候human()被调用
  3. 什么时候computer()被调用
  4. 如果一者的输入是另一者的输出,如何解决衔尾蛇般的循环依赖问题

那么我们来看看reactive stream吧。这个话题大家应该也有所了解。

Reactive Stream

Reactive

首先是reactive stream中的Reactive,关于这个词歧义有点多,首先就是汉语翻译就和布局中德responsive相同了,不过在坐都是专业前端,那就假设至少不合和responsive搞混。
我们假设module是一个封装有状态的对象,我们用一个A到B的箭头来表示A以某种方式影响了B中的状态。常见的例子的话,每向服务器发送一次请求,就给计数器+1。
那么问题是,这种情况,箭头这个关系的代码,是放在哪儿的?
典型的实现是这样,放在A里面,作为请求的回调的一行就行了。
这就表示,A影响B的关系是A所拥有的。
B是被动的passive,它允许其他module改变其状态;A是主动的proactive,它保证B的状态正确转换。被动的module根本意识不到箭头关系的存在。
而另一种方式就是,交换箭头关系的所有者,而不反转箭头指向。
这时候B监听A上面的状态,然后在A的状态改变事件发生时,管理自己的状态。
这时候B就是reactive的,它对某些事件进行react来维护自己的状态。A是listenable的,这次换A不知道箭头关系的存在了。
这样于是就成了 Inversion of Control,B只需要负责自己了。
同样A也不用去管其他组件了。
而且B也能将incrementCounter函数private了,再passive模式中,incrementCounter必须public。
如果想探究Passive模式下incrementCounter它如何使用,我们需要查找他在哪儿被调用。
如果想探究Reactive模式下,incrementCounter它如何使用(被什么module所影响),就得查找它依赖的事件存在什么模块中。
通常,命令式编程中常用Passive/Proactive,Reactive也会被零散使用。Reactive的主要卖点是构建自我负责的mudole,保证封装,主要关注自己的功能而非改变外部的状态。
亦即Separation of Concern,关注点分离。

Stream

解决了什么是Reactive,那么什么是Stream呢?
在Cycle.js的定义中:stream是一个持续发出0或者多个事件的事件流,可能结束,也可能无限;如果它结束了,它将会放出error或者特别的complete事件。

(next)* (complete|error){0,1}

一个stream是listenable的。但是他需要3个事件处理器。
一个用于常规事件,一个用于error,一个用于complete。

myStream.addListener({
  next: function handleNextEvent(event) {
    // do something with `event`
  },
  error: function handleError(error) {
    // do something with `error`
  },
  complete: function handleCompleted() {
    // do something when it completes
  },
});

然后大家熟知的Rx.js和Cycle.js钦点的xstream则是自带一堆将stream进行转换的operator:基于一个stream生成另一个stream的纯函数。

Cycle.js中的Stream

回到问题

具体sense和actuator的类型是什么?
计算机和人互相监听的衔尾蛇模型又意味着什么?

常见场景下,计算机就是生成像素的,人就是生成键鼠点击事件的。计算机监听人点击的事件,人监听计算机生成到显示器上的像素状态。然后这两者可以总结成两种Stream

  1. 计算机生成图像的stream
  2. 人生成键鼠点击的stream

人和计算机都在监听互相的输出。于是计算机端的程序可以这样表示

function computer(userEventsStream) {
  return userEventsStream
    .map(event => /* ... */)
    .filter(someCondition)
    .map(transformItToScreenPixels)
    .flatten();
}

至于人这一端,目前还无法编程。

不过事实也没这么简单,因为我们必须离开JS环境并影响现实世界。这也是函数式必须要副作用,这个弄脏设计的地方的原因。
在Cycle.js中,这个连接外部的桥梁称作Driver。
打个比方,Cycle.js中,DOM Driver接收计算机的图像stream,返回键鼠点击stream。这之中,DOM Driver产生write副作用在dom结中渲染出元素,捕获read用户交互产生的副作用。这样DOM Driver对于Cycle.js的而言,就代表了用户做出了DOM操作。
Driver这个词来源于Operating System Driver,其构建了硬件和软件的桥梁。
这里也回答了,computer和human函数在什么时候被调用的问题了。
不过在Cycle.js中,computer函数被用一个更常见的名字替代,main()。亦即程序启动的第一函数。
在Cycle.js中,human被driver所所代理,也就有了这个关系。

y = domDriver(x)
x = main(y)

不过=不是赋值,而是更像x = g(f(x))中的=,后者的x在右侧是undefined
这即是Cycle.js介入的地方。

function main(sources) {
  const sinks = {
    DOM: // transform sources.DOM through
         // a series of xstream operators
  };
  return sinks;
}

const drivers = {
  DOM: makeDOMDriver('#app') // a Cycle.js helper factory
};

run(main, drivers); // solve the circular dependency

你定义了maindrivers,接着使用run将他们连接。
这也是Cycle.js名字的来源:它是一个解决stream循环依赖的框架,人机交互中必然存在的stream循环。