前言
开卷,有点疲惫了
面试题:
从输入url到页面展示这段时间里发生了什么?
- URL输入与解析: 一般输入url的是域名,所以需要先解析成:确定协议(如HTTP或HTTPS)、主机名、端口号(如果有指定)、路径及查询参数。
- DNS查询=>逐级向上的过程
- 浏览器检查本地缓存是否有该域名对应的IP地址。
- 如果没有找到,浏览器会向操作系统发出DNS查询请求以获取对应域名的IP地址。
- 操作系统也会首先检查自身的DNS缓存,如果未命中,则会向配置的DNS服务器发送查询请求。
- DNS服务器返回域名对应的IP地址给浏览器。
- 建立TCP连接: 一旦获得了目标服务器的IP地址,浏览器将尝试与服务器建立TCP连接。如果是HTTPS请求,还会进行SSL/TLS握手来确保通信的安全性。(这时候可以讲讲三次握手、四次挥手)
- 发送HTTP/HTTPS请求:
- 建立连接后,浏览器构造HTTP请求(包含请求行、头部信息和可能的请求体)并发送给服务器。
- 请求可以是GET、POST等方法,取决于用户交互或网页加载的需求。 (可以聊聊GET和POST的区别)
- 服务器处理请求并返回HTTP报文
- HTTP报文也分成三份,状态码 ,响应报头和响应报文
- 从服务器请求的HTML,CSS,JS文件就放在响应报文中
- 浏览器接收响应:
- 浏览器接收到服务器的响应后,开始解析HTML文档内容。
- 在解析过程中,如果遇到外部资源引用(如CSS文件、JavaScript脚本、图片等),浏览器会发起额外的请求来获取这些资源。
- 构建DOM树和CSSOM树:
- 浏览器基于HTML内容构建DOM树。
- 同时,基于CSS样式表构建CSSOM树。
- 布局和绘制:
- 结合DOM树和CSSOM树,浏览器创建渲染树,并计算每个节点在屏幕上的确切位置和大小(布局阶段)。
- 根据渲染树的信息,浏览器将页面绘制出来(绘制阶段)。
9 JavaScript执行:
- 页面加载过程中,若存在JavaScript代码,浏览器会在适当的时候执行这些代码,这可能会修改DOM结构或应用新的样式规则,从而影响最终的页面展示。
那如果js一直堵塞,想要渲染动画怎么处理?
- 当时回答了使用Web Workers,Web Workers允许你在后台线程中运行脚本,从而避免长时间运行的任务阻塞主线程。但是Web Workers不能直接访问DOM,一般渲染动画都需要操作dom
- 后面想到了可以用css去渲染,回答了可以用到transform 的一些属性 (实际上是用@keyframes来制作动画)
面试官又问 CSS 动画为什么不被 JS 阻塞?
虽然JS是单线程,但是浏览器是多线程:
- 主线程:负责处理大部分网页内容,包括解析 HTML、执行 JavaScript、计算样式以及布局等。
- 合成器线程:专门负责页面的绘制和合成(compositing)。当动画仅涉及
transform和opacity属性时,这些属性的变化可以由合成器线程独立处理,无需经过主线程重新计算布局或样式。
如何减少白屏时间?
- 首先我回答了可以使用SSR 也就是 服务端渲染(Server-Side Rendering, SSR),静态资源可以在服务器端编译好,在发给浏览器从而让用户更快看到内容,同时也可以改善SEO。
- 使用骨架屏(Skeleton Screen), 在主要内容加载完成之前,显示一个简单的占位符布局(即骨架屏),给用户一种页面正在快速加载的感觉。
- 路由懒加载: 主要的作用在于打包的时候,可以减少压缩的体积
- 组件懒加载:是在需要的时候才动态加载组件,而不是在应用启动时就全部加载。这样可以减少初始加载时间,并且只在实际需要显示某个组件时才加载相关的JavaScript代码。
- 异步加载非关键资源
- 异步加载JavaScript:使用
<script async>或<script defer>属性,避免阻塞页面渲染。 - 懒加载图像和其他媒体:仅当用户滚动到相应部分时才加载图像或其他媒体文件。
v-for 使用时key 的作用是什么?
当时我主要从虚拟DOM去讲起,可以避免一些不必要的更新,当Vue执行虚拟DOM的diff算法时,它会利用key来匹配新旧虚拟DOM树中的元素。这使得Vue可以快速定位到哪些元素发生了变化,从而只更新必要的部分,而不是重新渲染整个列表。
作用:
1. 提高更新效率
当数据项发生变化(如添加、删除或重新排序)时,Vue 使用 key 来追踪每个节点的身份,以便尽可能地复用现有的 DOM 元素,而不是每次都重新创建它们。这可以显著提高列表更新的性能。
2. 确保组件状态正确
如果列表中的项目是动态组件或包含局部状态的组件,key 可以确保当数据变化时,正确的组件实例被销毁和重建。没有 key,Vue 可能会复用组件实例,导致状态混乱。
3. 避免不必要的重排
在不使用 key 的情况下,Vue 默认会尝试最小化对 DOM 的变动,但这可能导致不必要的重排(reflow)和重绘(repaint)。通过提供唯一的 key,Vue 能够更精确地决定哪些元素需要被更新,从而减少不必要的操作。
又问那key 对于v-if的作用呢?
强制重新渲染
- 当切换
v-if的条件时,默认情况下 Vue 会尝试复用现有的 DOM 元素以提高性能。这意味着如果条件从false变为true,Vue 可能会保留之前的 DOM 状态而不是完全重新创建一个新的。 - 如果你希望在条件变化时强制重新渲染(即销毁旧的实例并创建新的),可以通过给元素添加唯一的
key来实现。当key发生变化时,Vue 会认为这是一个全新的元素,从而触发完整的生命周期钩子(如mounted,destroyed等)和相应的DOM操作。
手写题:
对象扁平化
给出flattenObject具体实现
const nestedObj = {
a: 1,
b: {
c: 2,
d: { e: 3 }
}
};
const flattenedObj = flattenObject(nestedObj);
console.log(flattenedObj);
// 输出: { 'a': 1, 'b.c': 2, 'b.d.e': 3 }
实现:
function flattenObject(obj, parentKey = '', result = {}) {
// 遍历对象 obj 的所有可枚举属性
for (const key in obj) {
// 检查属性是否是对象自身的属性,而非原型链上的属性
if (obj.hasOwnProperty(key)) {
// 构建新的键名,如果 parentKey 存在,使用点号连接 parentKey 和当前 key
const newKey = parentKey ? `${parentKey}.${key}` : key;
// 检查当前属性值是否为非 null 的对象,并且不是数组
if (typeof obj[key] === 'object' && obj[key] !== null && !Array.isArray(obj[key])) {
// 递归调用 flattenObject 处理嵌套对象,更新 parentKey 和 result
flattenObject(obj[key], newKey, result);
} else {
// 如果是基本类型值,直接将新键和对应的值添加到 result 对象中
result[newKey] = obj[key];
}
}
}
// 返回扁平化后的对象
return result;
}
console.log( flattenObject(nestedObj)) //{ a: 1, 'b.c': 2, 'b.d.e': 3 }
解析:
hasOwnProperty方法确保只处理对象本身的属性,而不是其原型链上的属性。- 创建
newKey,如果parentKey存在,则通过点号连接它与当前key,否则直接使用key作为新的键名。 - 检查当前属性值是否为非
null的对象并且不是数组。如果是,则递归调用flattenObject函数,继续处理该嵌套对象,更新parentKey为newKey并将结果存储在同一个result对象中。
数组扁平化:
题目描述已有多级嵌套数组 : [1, [2, [3, [4, 5]]], 6,7] 将其扁平化处理 输出: [1,2,3,4,5,6,7]
实现: 只需要按照上面的模板我们来改改就好了
const nestedObj = [1, [2, [3, [4, 5]]], 6, 7];
function flattenObject(obj, result = []) {
if (!Array.isArray(obj)) {
result.push(obj);
return result;
}
for (const item of obj) {
if (Array.isArray(item)) {
flattenObject(item, result);
} else {
result.push(item);
}
}
return result;
}
console.log(flattenObject(nestedObj));// [ 1, 2, 3, 4, 5, 6, 7]
高级写法: 我去搜了大佬的文章来理解,见此篇==>>>juejin.cn/post/711876…
function flatten(arr) {
while (arr.some(item=> Array.isArray(item))) {
console.log(...arr)
arr = [].concat(...arr)
console.log(arr)
}
return arr
}
console.log(flatten(arr));
- 介绍一下some函数方法
.some()方法在 JavaScript 中用于测试数组中的至少一个元素是否满足提供的函数实现的条件。它是一个数组方法,返回一个布尔值:
- 如果找到一个数组元素满足提供的测试函数,则返回
true并停止遍历数组。 - 如果没有找到这样的元素,则返回
false。
案例
const numbers = [2, 4, 6, 8, 10, 12];
const anyOver10 = numbers.some(function(num) {
return num > 10;
});
console.log(anyOver10); // 输出 true 因为存在大于10的数字12
- 直接执行这个代码时
arr = [].concat(...arr)等价于arr = [].concat(1, [2, [3, [4, 5]]], 6, 7);因为扩展运算符...arr把原数组打散成了一个个参数传给.concat()。然后.concat()会把这些参数合并成一个新数组:
[1, 2, [3, [4, 5]], 6, 7]
- 讲讲执行流程:
第一次迭代
arr.some(item => Array.isArray(item)):item遍历到的第一个值是1,不是数组。- 接着
item是[2, [3, [4, 5]]],这是数组,所以.some()返回true,进入循环体。
console.log(...arr):- 输出原始数组中的所有元素:
1 [2, [3, [4, 5]]] 6 7
- 输出原始数组中的所有元素:
arr = [].concat(...arr):- 将
arr展开一层,结果为[1, 2, [3, [4, 5]], 6, 7]
- 将
第二次迭代
arr.some(item => Array.isArray(item)):- 现在
arr已经变成了[1, 2, [3, [4, 5]], 6, 7] item遍历到第一个非数组值1,继续。- 下一个
item是2,继续。 - 当
item是[3, [4, 5]]时,.some()返回true,因为这是一个数组,再次进入循环体。
- 现在
console.log(...arr):- 输出展开后的一层数组:
1 2 [3, [4, 5]] 6 7
- 输出展开后的一层数组:
arr = [].concat(...arr):- 再次将
arr展开一层,结果为[1, 2, 3, [4, 5], 6, 7]
- 再次将
第三次迭代
arr.some(item => Array.isArray(item))- 现在
arr变成了[1, 2, 3, [4, 5], 6, 7] item遍历到[4, 5],这是一个数组,因此.some()返回true,再次进入循环体。
- 现在
console.log(...arr):- 输出:
1 2 3 [4, 5] 6 7
- 输出:
arr = [].concat(...arr):- 最终展开为
[1, 2, 3, 4, 5, 6, 7]
- 最终展开为
总结: 如果面试真来了这题,我们先可以写第一个或者简单的方法,等面试问你能不能优化一下,或者你写完第一个方法,假装思考一下,再写第二种方法.
实现嵌套对象数组去重
实现嵌套对象数组去重:给出removeDuplicates
const nestedArr = [
{ a: 1, b: { c: 2 } },
{ a: 1, b: { c: 2 } },
{ a: 3, b: { d: 4 } }
];
const uniqueArr = removeDuplicates(nestedArr);
实现:
function deepEqual(a, b) {
if (a === b) return true;
if (typeof a !== 'object' || a === null || typeof b !== 'object' || b === null) {
return false;
}
const keysA = Object.keys(a);
const keysB = Object.keys(b);
if (keysA.length !== keysB.length) return false;
for (let key of keysA) {
if (!keysB.includes(key) || !deepEqual(a[key], b[key])) {
return false;
}
}
return true;
}
function removeDuplicates(arr) {
const result = [];
for (const item of arr) {
if (!result.some(existing => deepEqual(item, existing))) {
result.push(item);
}
}
return result;
}
const nestedArr = [
{ a: 1, b: { c: 2 } },
{ a: 1, b: { c: 2 } },
{ a: 3, b: { d: 4 } }
];
const uniqueArr = removeDuplicates(nestedArr);
console.log(uniqueArr);
// 输出: [ { a: 1, b: { c: 2 } }, { a: 3, b: { d: 4 } } ]
流程讲解:
- 当你调用
removeDuplicates(nestedArr)时:
nestedArr被遍历,每个元素都是一个对象。- 对于每个对象,使用
some方法和deepEqual函数检查这个对象是否已经在result数组中存在。 - 如果不存在,则将该对象添加到
result中。 - 最终,
result数组包含了所有唯一的对象,没有重复。
2.调用 deepEqual 时:
- 首先检查两个变量是否是同一个引用(即指向内存中的同一块地址)。如果是,则直接返回
true,因为它们肯定是相等的。 - 检查
a和b是否都是对象类型,并且都不是null。如果其中任何一个不是对象(例如数字、字符串、布尔值)或为null,则直接返回false,因为这种情况下只能通过严格相等(===)来判断,而前面已经排除了这种情况。 - 使用
Object.keys()方法获取对象a和b的所有可枚举属性名(键),并分别存储在keysA和keysB中。这一步是为了后续遍历每个对象的属性进行逐个比较做准备。 - 如果两个对象的键数量不同,则它们肯定不相等,因此直接返回
false。这是为了快速排除明显不相等的情况。 - 对于
a中的每一个键,首先检查该键是否也存在于b中(使用includes方法)。如果不存在,或者虽然存在但是对应的值不相等(通过递归调用deepEqual来判断),则返回false。 - 如果通过了上述所有的检查,说明两个对象在结构和内容上都是完全相同的,所以最终返回
true。