前几天面试的时候遇到一道面试题,还是挺考验能力的。
题目是这样的:
rpc 是 remote procedure call,远程过程调用,比如一个进程调用另一个进程的某个方法。很多平台提供的进程间通信机制都封装成了 rpc 的形式,比如 electron 的 remote 模块。
小程序是双线程机制,两个线程之间要通信,提供了 postMessage 和 addListener 的 api。现在要在两个线程都会引入的 common.js 文件里实现 rpc 方法,支持并发的 rpc 通信。
达到这样的使用效果:
constres=awaitrpc('method',params);
这道题是有真实应用场景的题目,比一些逻辑题和算法题更有意思一些。
实现思路
两个线程之间是用 postMessage 的 api 来传递消息的:
在 rpc 方法里用 postMessage 来传递要调用的方法名和参数
在 addListener 里收到调用的时候,调用 api,然后通过 postMessage 返回结果或者错误
我们先实现 rpc 方法,通过 postMessage 传递消息,返回一个 promise:
functionrpc(method,params){postMessage(JSON.stringify({method,params}));returnnewPromise((resolve,reject)=>{});}
这个 promise 什么时候 resolve 或者 reject 呢? 是在 addListener 收到消息后。那就要先把它存起来,等收到消息再调用 resolve 或 reject。
为了支持并发和区分多个调用通道,我们加一个 id。
letid=0;functiongenId(){return++id;}constchannelMap=newMap();functionrpc(method,params){constcurId=genId();postMessage(JSON.stringify({id:curId,method,params}));returnnewPromise((resolve,reject)=>{channelMap.set(curId,{resolve,reject});});}
这样,就通过 id 来标识了每一个远程调用请求和与它关联的 resolve、reject。
然后要处理 addListener,因为是双工的通信,也就是通信的两者都会用到这段代码,所以要区分一下是请求还是响应。
addListener((message)=>{const{curId,method,params,res}=JSON.parse(message);if(res){//处理响应}else{//处理请求}});
处理请求就是调用方法,然后返回结果或者错误:
try{constdata=global[method](...params);postMessage({idres:{data}});}catch(e){postMessage({id,res:{error:e.message}});}
处理响应就是拿到并调用和 id 关联的 resolve 和 reject:
const{resolve,reject}=channelMap.get(id);if(res.data){resolve(res.data);}else{reject(res.error);}
全部代码是这样的:
letid=0;functiongenId(){return++id;}constchannelMap=newMap();functionrpc(method,params){constcurId=genId();postMessage(JSON.stringify({id:curId,method,params}));returnnewPromise((resolve,reject)=>{channelMap.set(curId,{resolve,reject});});}addListener((message)=>{const{id,method,params,res}=JSON.parse(message);if(res){const{resolve,reject}=channelMap.get(id);if(res.data){resolve(res.data);}else{reject(res.error);}}else{try{constdata=global[method](...params);postMessage({idres:{data}});}catch(e){postMessage({id,res:{error:e.message}});}}});
我们实现了最开始的需求:
实现了 rpc 方法,返回一个 promise
支持并发的调用
两个线程都引入这个文件,支持双工的通信
其实主要注意的有两个点:
要添加一个 id 来关联请求和响应,这在 socket 通信的时候也经常用
resolve 和 reject 可以保存下来,后续再调用。这在请求取消,比如 axios 的 cancelToken 的实现上也有应用
这两个点的应用场景还是比较多的。
总结
rpc 是远程过程调用,是跨进程、跨线程等场景下通信的常见封装形式。面试题是小程序平台的双线程的场景,在一个公共文件里实现双工的并发的 rpc 通信。
思路文中已经讲清楚了,主要要注意的是 promise 的 resolve 和 reject 可以保存下来后续调用,通过添加 id 来标识和关联一组请求响应。