12-面试讲解
# 跨标签页通信
# 面试讲解
# 知识点图谱
# 难点描述
**模拟问题:**我看你的简历里面写了对于跨标签页的通信,能聊一下这个吗?
问题分析:
跨标签页的通信本身实现并不复杂,特别是如果是通过
BroadCastChannel
方式的实现。但是像跨页面通信的这种问题,并不像我们之前讲解的微前端,前端监控这种,本身就是一个比较大的概念,所以我们需要多站在宏观的角度去聊问题。而这个问题,需要给面试官展示的是思考解决问题的过程,对知识的掌控,虽然你在了解了BroadCastChannel这个API之后,可能自己会觉得很简单,但是你简单的泛泛而谈,和你引经据典并且包含着具体解决方案的去谈,给人的感觉是不一样的参考答案:
在我们的项目中,有这样子的需求,在其中一个标签页上做了操作,其他页面需要同步对应的状态。这其实就涉及到浏览器的跨标签页通信了。
(思考过程)其实一开始考虑的方式是通过
WebSocket
来实现,不过WebSocket
需要务器端的支持,而我们需要的仅仅是一个纯前端的支持,所以直接pass掉了。当然就考虑到了LocalStorage
,通过事件window.onstorage
来监听,只要其他标签页修改了LocalStorage
中的内容,window.onstorage
就会被触发,而且事件返回的数据也非常全面,我甚至用LocalStorage
已经实现了一版,不过这个实现还是有比较明显的短板,我们想要的是实时通信,但是LocalStorage
实际最大的作用是共享数据,而且随着我们的操作变多,LocalStorage
中存放的内容也越来越多。后面又发现有SharedWorker
,不过没有过多的研究,因为看到了兼容性就直接否定了。**(确定解决方案)**最后发现了有
BroadCastChannel API
,研究了一下就发现,这个API完全和浏览器标签通信的原理契合的(埋钩子),可以很简单方便的实现跨标签页通信,最后我们技术方案的选择就是BroadCastChannel
不过具体的细节在工程化环境中还是有一些注意的点,特别是在我们现在响应式数据的工程中,像Vue方式处理的响应式数据,不能够直接通过
BroadCastChannel
管道进行传递,这是由于浏览器页签之间传递克隆数据原因引起的(埋钩子,引起面试官的询问,也可以带出另外的话题点)。如果退出页面,
BroadcastChannel
的监听也需要退出,所以,如果是单独封装的类或函数,需要单独处理退出关闭的,一般我们会做闭包处理(埋钩子,很有可能引起另外一个谈论的话题点)。再返回一个函数即可还有就是如果页面上这样的需求比较多,那么管道的命名需要规范,毕竟在同一个域下面,
BroadCastChannel
管道的名字是唯一的,我们指定了相关的规范协议,来统一管道的命名**(处理结果)**通过这样处理之后,不需要复杂的处理,不需要共享数据,也不需要经过后端的中转。我们就可以直接在前端实现跨标签页的通信,而且通信还是实时的。最重要的这个API的兼容性还行,只有IE浏览器不兼容,不过我们的项目这些特殊的需求本身也没有考虑老旧的浏览器,所以用起来都非常舒畅
(主动提问:看面试官还有什么细节需要了解)
# 知识点叙述
1、BroadcastChannel
的原理
**模拟问题:**你用到了BroadcastChannel
,那你知道实现它的基本原理吗?
问题分析:
这个基本上是我们埋下的钩子,引导面试官来问这个问题,表示我们不仅仅只了解了API的表面,实际掌握了基本的原理。
参考答案:
BroadCastChannel API
,他的原理就是一个命名管道,我们的浏览器每个标签页本身就是一个独立的进程,基于管道的通信,本来就是进程之间的通信方式之一。所以这个玩意就是为跨标签页通信存在的。具体API实现比较的简单,既然原理是命名管道,所以每个
BroadcastChannel
对象都需要使用一个唯一的名称来标识通道。一个页面通过postMessage
方法将消息发送到频道中,而其他页面则可以监听message
事件来接收这些消息,所以实现起来不存在难度。
**模拟问题:**你刚才说浏览器每个标签页本身就是一个独立的进程,这是什么意思?
问题分析:
同样考察的是基本常识
参考答案:
每个浏览器标签页通常被视为一个独立的进程,而不是一个线程。这种多进程架构被称之为多进程浏览器,谷歌浏览器就是采用这种方式。
这种架构的方式的主要目的是提高浏览器的稳定性、安全性和性能。
在多进程浏览器中,每个标签页都独立运行在独立的进程中,这样一旦一个标签页崩溃或遇到问题,不会影响其他标签页和浏览器本身的稳定性。而每个进程都有属于自己的内存。
不过进程之间的通信就稍微麻烦了一些,在多进程浏览器中,不同标签页之间的通信是通过进程间通信
IPC
机制来实现的(Inter-Process Communication,指至少两个进程间传送数据或信号的一些技术或方法)实现通信的方式一般是通过:管道,信号
Signal
,消息队列,套接字Socket
,共享内存等等而BroadCastChannel API实现的基本原理其实就是命名管道,和进程的通信方式十分的契合
2、BroadcastChannel
克隆
**模拟问题:**你刚刚说的细节问题中,提到了克隆的问题,BroadcastChannel通信为什么还和克隆有关系?
问题分析:
这是上面我们回答时候埋下的钩子,如果面试官不问的话,这个也可以一开始自己就说出来。
参考答案:
因为不同标签页之间是通过消息传递数据,传递的消息可以是任意的类型,当然也可以是对象类型,而这对象类型我们不可能直接赋值传递的。因为不同的进程中不可能存放同一个地址的数据。因此肯定是做克隆传递。而响应式数据,比如在Vue3中都是通过Proxy做了代理的,那么在做克隆的时候就会出现问题,不能对Proxy代理的对象进行克隆,知道这个原理之后,解决也很简单,消除Vue3数据的响应式即可,可以直接使用展开运算符,也可以使用vue给我们提供的方法toRaw。
**模拟问题:**你刚刚提到了克隆,你能说说什么是浅克隆,什么是深克隆吗?实现浅克隆,深克隆有哪些方式?
问题分析:
这也是我们埋下的钩子,浅克隆,深克隆,算是一个非常常见的八股问题,但是埋在我们具体的难点亮点的项目中,才更具有意义。
参考答案:
浅克隆创建的新对象,只复制一层深度的属性。对于嵌套对象或数组,浅克隆只复制对象引用,而不是嵌套对象的实际内容。
深克隆创建的新对象,是原始对象的完全独立副本,包括嵌套对象的所有内容。深克隆复制所有嵌套对象和数组的内容,使新对象与原始对象完全分离。修改深克隆后的对象不会影响原始对象,反过来也一样。
在我们一般的操作中,浅克隆一般是:
Object.assign(),展开运算符 (
...
),Array.prototype.slice(),Array.prototype.concat()深克隆操作的话,如果是简单对象,可以直接使用JSON.parse(JSON.stringify()),
不在意兼容性的话也可以使用新的 Web API,structuredClone,大多数复杂的结构都支持,只是不支持克隆函数和 DOM 节点
所以一般,我们可以自己实现,当然,也能使用第三方库,比如
lodash
的实现
const obj = {
re: /hello/,
f() {},
date: new Date(),
map: new Map(),
list: [1, 2, 3],
a: 3,
b: 4,
};
const obj2 = { loop2: obj, name: "obj2" };
obj.loop = obj2;
function deepClone(source, cache = new WeakMap()) {
//原始类型或函数直接返回
if (typeof source !== "object") {
return source;
}
//加入缓存解决循环引用
if (cache.has(source)) {
return cache.get(source);
}
let res = new source.constructor();
cache.set(source, res);
//处理JS内置数据结构:Array、Map、Set、Object
if (source instanceof Array) {
source.forEach((v) => {
res.push(deepClone(v, cache));
});
} else if (source instanceof Map) {
for (const [k, v] of source) {
res.set(k, deepClone(v, cache));
}
} else if (source instanceof Set) {
for (const v of source) {
res.add(deepClone(v, cache));
}
} else if (Object.prototype.toString.call(source) == "[object Object]") {
for (const key in source) {
res[key] = deepClone(source[key], cache);
}
} else {
//处理自定义对象(需遵循协议new constructors时为深拷贝)
res = new source.constructor(source);
}
return res;
}
const newObj = deepClone(obj);
console.log(newObj);