rxjs入门学习笔记

之前早就听说并简单看过响应式函数编程,但并没有仔细的学习并写过代码练习,(其实作为一个程序员特别是跟UI打交道的程序员,挺不应该的…响应式函数编程号称是找到了解决UI编程复杂性的方法),今天抽时间学习练习一下(比较浅薄的记录,对rxjs很熟悉的人就不要看了…)。

先了解一些基本概念,
Functional programming: 函数式编程,参考维基百科链接

Reactive Programming: 响应式编程,参考维基百科链接

Functional Reactive Programming: 响应式函数编程, 参考维基百科链接
简单说就是: 函数式编程+响应式编程, 思想来自于微软。在各个语言都有其实现, OC和Swift有ReactCocoa, Java有RxJava, Javascript有Rxjs。

为什么选择Javascript来学习呢?因为我猜Javascript更有可能统一宇宙吧…

先学习一下rxjs中的一些基本概念(当然其他语言的实现中也包含这些基本概念)。

Observable
一组可被观测的值,可以用来表示未来的值或者事件
Observer
观察者,知道如何处理来自Observable的值
Subscription
订阅,表示Observable的执行,Observable只有在subscribe时才开始执行
Operators
纯函数,用来处理和转换Observable发出的事件流
Subject
既是Observer又是Observable,可以用来实现广播的功能
Schedulers
用来集中处理并发,允许我们控制订阅的执行的时机和事件通知的时机

不太想也没办法一开始就把所有概念理解的非常清楚,在对基本的概念有了大致了解的基础上,还是动手来写点代码会更有助于理解一些。

因此考虑实现一个比较常见的业务场景来使用下rxjs: 搜索框输入文字,在输入的过程中,边输入边查询服务器的服务并显示输入提示的信息。
这个场景看似比较简单,但如果完全正确实现,还是有一些细节要考虑:
1. 监听输入框的文本变化
2. 查询服务器搜索提示服务
3. 取得结果并显示出来
4. 为了防止每次输入都去请求服务器造成浪费,需要输入以后延迟一小段时间(例如500ms)再去请求服务器
5. 请求服务器过程是异步的,在请求过程中,如果输入框内容变化了,请求结果不应该显示出来

按这些点分别讲一下实现:
1)监听输入框文本变化事件, 通过查文档,rxjs有一个fromEvent方法可以将dom事件转成Observable
2) 先用Mock的方法模拟一个服务器请求和相应的结果
3) 直接显示结果
4) 仔细查了下,在Operator里有一个throttleTime可以实现延迟执行,在规定时间内只执行1次的需求
5) 请求结果带请求时的输入参数,如果请求时输入参数和返回时输入参数有了变化,则不更新搜索提示

写的比较简单,但实际还是摸索了很久/(ㄒoㄒ)/

源码如下:


<div> <br /> <span>{{recommendResult}}</span> </div> import { fromEvent, asyncScheduler } from 'rxjs'; import { throttleTime, map, switchMap } from 'rxjs/operators'; export default { name: 'HelloWorld', data() { return { recommendResult: '', }; }, mounted() { function mockApiCall(text) { return new Promise((resolve) =&gt; { setTimeout(() =&gt; { resolve({ query: text, recommend: `recommend of ${text}`, }); }, 300); }); } fromEvent(this.$refs.input, 'input') .pipe( throttleTime(500, asyncScheduler, { leading: false, trailing: true }), map(() =&gt; this.$refs.input.value), switchMap(text =&gt; mockApiCall(text)), ) .subscribe((result) =&gt; { if (result.query === this.$refs.input.value) { this.recommendResult = result.recommend; } }); }, };

几个小坑:
1. rxjs 最新的api和之前的差不少,所以网上的一些文章(没标明版本的)不太好参考,还是看官方文档比较靠谱
2. throttleTime 如果不加参数,默认是{ leading: true, trailing: false }这样的效果就是,输入一开始就有一个事件出来,但是最终停止了是没有事件的,但我们想要的是停止的时候需要有一个事件,因此要改成{ leading: false, trailing: true }
3. mock的api请求是基于promise的接口,想融入到Observable的事件流里边,花了一番精力,理解文档,上网搜,才知道用switchMap这个Operator可以实现

看源码,核心的控制整个事件监听和响应的流程的代码一共10行左右,提到的那些小点也都满足了,整个控制流程不会散落在各个函数里边,代码还是比较清晰的,更少的代码代表着更少的bug,不过如果对rx不熟的人理解起代码来或者上手还是比较费劲的。

思考: 第4点其实有更合理的方式,应该是输入停止一小段时间(例如300ms)再去请求服务器,这样如果用户一直在快速输入,其实是不需要任何请求的,如果采用这种方式,如何用rxjs来实现呢?

注: 文中rxjs版本基于6.5.2, 主要参考自官网: rxjs官网链接

浏览器的’事件循环’机制


事件循环 (event loop)本来是JavaScript里一个非常重要的概念,大体意思就是JavaScript会不断地循环一个任务队列,取出队列里的第一个任务执行,用代码描述如下:


while(true){ let task = queue.pop(); exec(task); }

如果单纯讲JavaScript的事件循环,好像这就够了 。但是解释不了JavaScript的异步运行机制,所以今天我们将JavaScript的事件循环机制和浏览器结合在一起,看看JavaScript在浏览器里是如何运行的。

我们知道,JavaScript是一门单线程语言,在任意时刻都只能做一件事件,那为什么在发出ajax请求、设置定时器、执行io操作的时候,也可以接着做其他任务呢?

原因很简单,因为浏览器是多线程的。

在浏览器里,除了有执行JavaScript的主线程外,还有处理dom事件的事件线程;处理定时任务的时间线程;处理ajax请求的网络线程;处理io操作的io线程(部分浏览器支持DB操作)

事件线程

在浏览器里,渲染出来的dom元素都具有监听用户操作的属性,比如用户点击事件:click,鼠标移动事件:mousemove等。

document.addEventListener('click',()=>{
    console.log('document click');
});

当我们在dom上添加事件监听的时候,实际上是给dom所监听的事件注册回调函数。
如果dom监听到回调函数的话,会把回调函数放进任务队列(queue)里,注意,是事件线程将回调函数扔进任务队列的。

时间线程

时间线程用于处理定时任务,如setTimeout,setInterval。
setTimeout和setInterval处理机制是一致的,区别在于setTimeout只执行一次,setInterval可以循环执行。

console.log(1);
setTimeout(()=>{
    console.log(2)
},200)

setTimeout(()=>{
    console.log(3)
},100)
let then = new Date().getTime();
while(new Date().getTime() -then < 1000){

}
console.log(4)

如上,当代码执行到setTimeout(arg1,arg2)的时候,会将此任务交给时间线程处理,主线程继续执行后面的代码。告诉时间线程,xxxms后,将回调放入任务队列(queue)里,xxx为函数第二个参数,回调为第一个参数。注意,是时间线程将回调函数扔进任务队列的。

网络线程

网络线程用于处理网络请求,如ajax请求,script,image请求等。
当网络请求成功(或失败),在监听请求状态变化后(比如xhr请求的readyState、script或者image的load事件),将回调扔进任务队列(queue)里。

IO线程

IO线程主要处理文件读写,DB操作等。
和网络线程类似,操作完成后将回调扔进任务队列里。

以上4类都是将回调放进宏任务队列(macro task)
JavaScript还有一个概念叫微任务(micro tasks),比如process.nextTick, Promise, Object.observer, MutationObserver等产生的回调,都会放进微任务队列里,微任务会优先于宏任务。

##总结

  1. JavaScript主线程其实是一直执行同步任务
  2. 所有的异步任务都会交给其他线程处理
  3. 线程处理完后将回调函数(如果有回调)扔进任务队列里
  4. JavaScript同步任务执行完毕后,优先选择微任务队列里的任务执行
  5. 轮询任务队列,如果有任务,取第一个执行,如果没有,继续轮询。
  6. 重复1-5

如何实现一个简单的WebViewJavascriptBridge

在移动应用的开发的过程中,经常会有应用内部的活动,以及部分对灵活性要求很高的功能会采用基于WebView的方式实现,用来免去应用提交到应用市场的审核时间,以及用户的升级成本。这种最基础的Hybrid应用场景中,WebView页面中的javascript环境经常需要共享native环境(iOSAndroid)中的部分数据,例如: 用户登录授权信息、应用的版本信息等等,以及需要借助native的能力实现某些功能,例如: 录音、拍照、GPS位置信息、蓝牙等等,还有另外的场景是重用部分native已经实现的功能,例如:跳转到App内其他页面,调起native的某个功能弹窗等等,因此提供一个js native交互的通道是非常基础的一个需求。

原理

如果对AndroidiOS 的相应的WebViewapi比较熟悉的话,有这么几个API可以实现javascriptnative之间的互相调用。

Android下边的android.webkit.WebView, javascript调用native的实现方式:

首先在native端调用addJavascriptInterface (Object object, String name),将Java对象object注册到Javascript环境的主Frame上,这样就可以在javascript中调用到nativeJava对象的方法。为了安全方面的考虑在JELLY_BEAN_MR1 版本以上的Android系统中,只有Java对象的公开的并且有JavascriptInterface注解的方法才能被Javascript调用到。

native调用Javascript的方式:

KitKat版本之后,WebView提供了一个evaluateJavascript方法,执行传入的字符串表示的js代码并能返回结果。在这个版本之前只能通过loadUrl(“javascript:”)这种方式执行js代码。

iOS8新加入了WebView的实现WKWebView, 现在基本上应用支持到iOS8以上就够了,因此这里只考虑WKWebView。在WKWebViewjs调用native端可以通过WKUserContentControlleraddScriptMessageHandler:name实现,在native中调用这个方法将相应的handler添加到WKWebViewWKUserContentController上,在Javascript中就可以通过调用window.webkit.messageHandlers.handlerName.postMessagejs消息发送到nativeWKWebView同样实现了一个跟AndroidWebView类似的evaluateJavaScript的方法,用来在native端直接在WKWebViewjs环境执行字符串表示的Javascript代码。

业内的开源实现

当然业内也有比较出色的开源实现,最出名的是marcuswestin/WebViewJavascriptBridge, 这是一个iOS下边的WebViewJavascriptBridge库实现,实现了一个iOSjs-native双向调用的bridge,可以支持UIWebViewWKWebView。这个项目的解析在网上很多,在这里不再赘述。只讲几个很关键的设计:

1) 执行环境的保持

jsnative的互相调用过程中,如果能保持执行环境,以js调用native举例,如果可以封装给上层一个调用接口,同时支持方法回调,这样上层的代码写起来就会非常顺畅。这个的实现其实也比较简单,给回调生成一个唯一id,在js端保存这个id和回调方法的映射,将id传给native, native端处理完,回调的时候同时将这个id传回给js端,这样js端就能找到相应的回调函数并执行。

2) 消息排队机制

native端和js端,这个库都实现了一个消息排队机制,用来缓存bridge准备好之前的消息,这样上层就可以不再关心bridge是否准备好了的逻辑,更方便调用。

3) iFrame的方式

js端新插入了一个不可见的iFrame,通过修改这个iFramesrc为自定义的schema来通知native端更新,这样可以保证完全不影响任何主Frame的逻辑。当然这种方式主要受限于UIWebView的接口,只能通过修改src的方式来让native端监听到变化,如果是WKWebView则完全没有必要用这种方式处理。

简化的设计

我们希望做一个iOS/Android下边通用的WebViewJavascriptBridge,在js端、iOS端、Android端统一接口,因此并不能直接使用这个开源实现。理论上我们可以根据这个库的实现,在js端稍作修改,判断Android环境上走javascript调用java的方法,而不是修改iFramesrc,同时在Java端解析发过来的消息,跟iOS端一样的解析和处理逻辑,就能实现三端统一的接口。

但是,这是最简单的方式吗?结合我们自己的需求,因为不需要兼容UIWebView, iOS端和Android端都可以在一开始就将相应handler注册进去,所以js一开始加载就可以调用到相应的handler,在js端不需要做消息排队机制,完全可以直接调用。同样因为不用兼容UIWebViewjs也就没必要创建iFrame来实现通知native端消息。那么native端消息排队的机制是必须的吗?如果native主动调用javascript,而javascript加载是异步的,native端除非限制只能在页面加载完之后再调用,否则必须增加消息排队机制,来缓存bridge准备好之前的调用消息。

我们考虑软件分层设计,把客户端作为更为基础的不易变的下层,把h5做为更灵活易变的上层,通常下层是不允许主动调用上层的,而是提供好基础的接口,等待上层调用,如果有状态变化或者其他需要通知上层的,通常是通过上层注册通知回调、消息监听等机制来实现。因此我们应该只需要实现js调用native的方法,并支持方法回调的异步处理机制,以及支持消息监听机制即可满足所有需求。

js调用native的方法可以很自然的联想到RPC(远程过程调用)js等价于RPC中的客户机,native等价于RPC中的服务端,中间的传输不是经过网络而是经过webview提供的js环境和native环境之间的字符串传输。因此可以参考RPC的设计,js需要实现的是每次调用生成唯一id,组装方法名和参数,保存这次调用的回调方法,将这些信息:调用id,调用方法名、方法参数转成字符串,传递给nativenative需要实现的就是接收方法名、方法参数,然后找到对应的方法并执行,并将执行结果返回给jsjs再根据客户端传回的调用id和结果信息找到相应的回调方法并执行。实现js监听native的事件也非常简单,对比RPC调用的每次调用一次回调,监听的方式是一次调用(或者说注册监听),可以允许多次回调,这样native需要将js端注册的回调记录下,当事件发生时,多次调用这个回调,js端在收到native的回调成功后不把回调方法移除,这样就能够收到native的多次回调。

模块化

如果native提供方法很多,则需要根据不同的业务来提供不同的方法实现。可用的模块化方式很多,比如最基本的每个模块自己注册js方法的处理函数的方式,在iOS下也可以利用OCruntime机制将js调用的方法名映射到OC中的方法,在Android下类似,也可以利用Java的反射机制将js调用的方法名映射成Java的类和方法名。

安全性

如果我们nativewebview打开了一个第三方的网站,如果这些方法暴露给了这些第三方网站的js,假设native实现的方法返回了一些敏感信息,那么第三方网站就能获取这些敏感信息。因此native需要判断调用的合法性,我们自己网站可以全部支持httpsnative需要禁止任何http站点的调用,强校验站点的https证书,只有自己站点才允许调用native提供的方法。当然native提供的方法最好不要返回任何敏感信息。