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