面試題:實(shí)現(xiàn)小程序平臺(tái)的并發(fā)雙工 Rpc 通信
前幾天面試的時(shí)候遇到一道面試題,還是挺考驗(yàn)?zāi)芰Φ摹?/p>
題目是這樣的:
rpc 是 remote procedure call,遠(yuǎn)程過(guò)程調(diào)用,比如一個(gè)進(jìn)程調(diào)用另一個(gè)進(jìn)程的某個(gè)方法。很多平臺(tái)提供的進(jìn)程間通信機(jī)制都封裝成了 rpc 的形式,比如 electron 的 remote 模塊。
小程序是雙線程機(jī)制,兩個(gè)線程之間要通信,提供了 postMessage 和 addListener 的 api?,F(xiàn)在要在兩個(gè)線程都會(huì)引入的 common.js 文件里實(shí)現(xiàn) rpc 方法,支持并發(fā)的 rpc 通信。
達(dá)到這樣的使用效果:
- const res = await rpc('method', params);
這道題是有真實(shí)應(yīng)用場(chǎng)景的題目,比一些邏輯題和算法題更有意思一些。
實(shí)現(xiàn)思路
兩個(gè)線程之間是用 postMessage 的 api 來(lái)傳遞消息的:
- 在 rpc 方法里用 postMessage 來(lái)傳遞要調(diào)用的方法名和參數(shù)
- 在 addListener 里收到調(diào)用的時(shí)候,調(diào)用 api,然后通過(guò) postMessage 返回結(jié)果或者錯(cuò)誤
我們先實(shí)現(xiàn) rpc 方法,通過(guò) postMessage 傳遞消息,返回一個(gè) promise:
- function rpc(method, params) {
- postMessage(JSON.stringify({
- method,
- params
- }));
- return new Promise((resolve, reject) => {
- });
- }
這個(gè) promise 什么時(shí)候 resolve 或者 reject 呢?是在 addListener 收到消息后。那就要先把它存起來(lái),等收到消息再調(diào)用 resolve 或 reject。
為了支持并發(fā)和區(qū)分多個(gè)調(diào)用通道,我們加一個(gè) id。
- let id = 0;
- function genId() {
- return ++id;
- }
- const channelMap = new Map();
- function rpc(method, params) {
- const curId = genId();
- postMessage(JSON.stringify({
- id: curId,
- method,
- params
- }));
- return new Promise((resolve, reject) => {
- channelMap.set(curId, {
- resolve,
- reject
- });
- });
- }
這樣,就通過(guò) id 來(lái)標(biāo)識(shí)了每一個(gè)遠(yuǎn)程調(diào)用請(qǐng)求和與它關(guān)聯(lián)的 resolve、reject。
然后要處理 addListener,因?yàn)槭请p工的通信,也就是通信的兩者都會(huì)用到這段代碼,所以要區(qū)分一下是請(qǐng)求還是響應(yīng)。
- addListener((message) => {
- const { curId, method, params, res}= JSON.parse(message);
- if (res) {
- // 處理響應(yīng)
- } else {
- // 處理請(qǐng)求
- }
- });
處理請(qǐng)求就是調(diào)用方法,然后返回結(jié)果或者錯(cuò)誤:
- try {
- const data = global[method](...params);
- postMessage({
- id
- res: {
- data
- }
- });
- } catch(e) {
- postMessage({
- id,
- res: {
- error: e.message
- }
- });
- }
處理響應(yīng)就是拿到并調(diào)用和 id 關(guān)聯(lián)的 resolve 和 reject:
- const { resolve, reject } = channelMap.get(id);
- if(res.data) {
- resolve(res.data);
- } else {
- reject(res.error);
- }
全部代碼是這樣的:
- let id = 0;
- function genId() {
- return ++id;
- }
- const channelMap = new Map();
- function rpc(method, params) {
- const curId = genId();
- postMessage(JSON.stringify({
- id: curId,
- method,
- params
- }));
- return new Promise((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 {
- const data = global[method](...params);
- postMessage({
- id
- res: {
- data
- }
- });
- } catch(e) {
- postMessage({
- id,
- res: {
- error: e.message
- }
- });
- }
- }
- });
我們實(shí)現(xiàn)了最開(kāi)始的需求:
- 實(shí)現(xiàn)了 rpc 方法,返回一個(gè) promise
- 支持并發(fā)的調(diào)用
- 兩個(gè)線程都引入這個(gè)文件,支持雙工的通信
其實(shí)主要注意的有兩個(gè)點(diǎn):
- 要添加一個(gè) id 來(lái)關(guān)聯(lián)請(qǐng)求和響應(yīng),這在 socket 通信的時(shí)候也經(jīng)常用
- resolve 和 reject 可以保存下來(lái),后續(xù)再調(diào)用。這在請(qǐng)求取消,比如 axios 的 cancelToken 的實(shí)現(xiàn)上也有應(yīng)用
這兩個(gè)點(diǎn)的應(yīng)用場(chǎng)景還是比較多的。
總結(jié)
rpc 是遠(yuǎn)程過(guò)程調(diào)用,是跨進(jìn)程、跨線程等場(chǎng)景下通信的常見(jiàn)封裝形式。面試題是小程序平臺(tái)的雙線程的場(chǎng)景,在一個(gè)公共文件里實(shí)現(xiàn)雙工的并發(fā)的 rpc 通信。
思路文中已經(jīng)講清楚了,主要要注意的是 promise 的 resolve 和 reject 可以保存下來(lái)后續(xù)調(diào)用,通過(guò)添加 id 來(lái)標(biāo)識(shí)和關(guān)聯(lián)一組請(qǐng)求響應(yīng)。