23. 客户端访问https的整个流程
客户端访问 HTTPS 网站的整个流程可以分为以下步骤:
-
DNS 解析: 客户端首先通过 DNS 解析将域名解析成对应的 IP 地址。
-
建立 TCP 连接: 客户端使用解析到的 IP 地址,通过 TCP 协议与服务器建立连接。这是一个三次握手的过程,包括客户端向服务器发送 SYN 报文,服务器回应 SYN+ACK 报文,客户端再回应 ACK 报文,完成连接的建立。
-
TLS 握手: 客户端发起 TLS 握手过程,其中包括以下步骤:
- 客户端发送 ClientHello 报文,其中包含支持的加密算法、协商密钥等信息。
- 服务器收到 ClientHello 报文后,选择一个加密套件,并发送 ServerHello 报文,确认加密套件。
- 服务器发送服务器证书,证明自己的身份。
- 如果需要,服务器还可以发送 ServerKeyExchange 报文,包含协商密钥所需的参数。
- 客户端验证服务器证书的有效性,并生成随机数,用服务器公钥加密后发送给服务器,用于协商对称密钥。
- 服务器使用自己的私钥解密客户端发送的随机数,并生成对称密钥,用客户端的公钥加密后发送给客户端。
- 客户端收到服务器发送的加密后的对称密钥,使用预先协商的对称密钥解密后,生成会话密钥。
- 客户端和服务器根据协商好的加密套件和会话密钥,开始加密通信。
-
HTTP 请求: 客户端使用建立好的安全连接,向服务器发送 HTTPS 请求,包括请求报文和请求头信息。
-
服务器处理请求: 服务器收到客户端发送的 HTTPS 请求后,根据请求信息处理请求,并生成响应报文和响应头信息。
-
HTTP 响应: 服务器将处理后的响应报文和响应头信息发送给客户端。
-
关闭连接: 客户端和服务器之间的通信完成后,通过 TCP 协议完成四次挥手,包括客户端向服务器发送 FIN 报文,服务器回应 ACK 报文,服务器向客户端发送 FIN 报文,客户端回应 ACK 报文,完成连接的关闭。
以上是客户端访问 HTTPS 网站的基本流程。在整个流程中,通过 TLS 握手过程建立安全连接,确保了客户端与服务器之间的通信是加密的、安全的。
3. 进程和线程的区别
进程(Process)和线程(Thread)是操作系统中管理任务和资源的基本单位,它们之间有以下区别:
-
定义:
- 进程是程序的一次执行过程,是系统中资源分配的最小单位,包括代码、数据和进程控制块等。
- 线程是进程中的一个实体,是 CPU 调度的最小单位,包括程序计数器、寄存器集合和栈等。
-
资源拥有:
- 进程拥有独立的地址空间、内存、文件描述符、设备和上下文等资源。
- 线程共享所属进程的地址空间和其他资源,但拥有独立的栈和寄存器。
-
切换开销:
- 进程切换的开销较大,需要保存和恢复大量的上下文信息,如 CPU 寄存器、内存映射等。
- 线程切换的开销较小,因为线程共享了大部分资源,只需要保存和恢复少量的线程私有信息即可。
-
并发性:
- 进程之间是独立并发执行的,相互之间不受影响,通信和同步需要额外的机制。
- 线程之间共享同一进程的资源,可以更方便地实现通信和同步,但也容易引发竞态条件和死锁等问题。
-
生命周期:
- 进程的生命周期是相对独立的,进程的创建、运行、等待、结束等操作都是由操作系统负责管理的。
- 线程的生命周期依赖于所属进程,线程的创建、等待、结束等操作通常由线程自己负责,但进程结束时,其所有线程也会被终止。
总的来说,进程是操作系统中资源分配的基本单位,而线程是操作系统中执行调度的基本单位。进程之间相互独立,线程之间共享资源,通过合理的组合使用,可以更有效地利用系统资源,提高系统的并发性和性能。
3. useEffect详细,以及第二个参数,第一个参数是什么函数?什么叫副作用函数?
在 React 中,useEffect 是一个 Hook,用于在函数组件中执行副作用操作。副作用操作通常包括订阅数据、设置定时器、手动修改 DOM 等,这些操作都是在组件渲染过程中无法在 render 函数中完成的。useEffect 可以让我们在函数组件中进行类似于生命周期函数中的操作。
useEffect 接收一个函数作为第一个参数,这个函数被称为副作用函数。当组件渲染时,React 会记住副作用函数,并在组件更新后执行它。副作用函数可能会产生一些副作用,例如发起网络请求、订阅外部数据源、手动修改 DOM 等。
useEffect 还可以接收一个可选的第二个参数,这个参数是一个依赖数组,用于指定副作用函数依赖的变量。当这些变量发生变化时,React 会重新执行副作用函数;如果依赖数组为空,则副作用函数只在组件第一次渲染和销毁时执行。
下面是 useEffect 的基本用法示例:
import React, { useEffect, useState } from 'react';
function Example() {
const [count, setCount] = useState(0);
// 副作用函数:在组件渲染后更新文档标题
useEffect(() => {
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
在上面的示例中,useEffect 用于在组件渲染后更新文档标题。由于没有指定第二个参数,因此副作用函数会在组件的每次渲染后都执行,这意味着每次点击按钮后都会更新文档标题。
3. vue的生命周期函数?
Vue.js 组件的生命周期函数是一组钩子函数,用于在组件的不同阶段执行代码。这些生命周期函数可以让你在组件的创建、挂载、更新和销毁等阶段执行特定的操作。以下是 Vue.js 2.x 中常见的生命周期函数:
-
beforeCreate: 在实例初始化之后,数据观测 (data observer) 和 event/watcher 事件配置之前被调用。此时组件实例还未创建,无法访问
data、methods等属性。 -
created: 在实例创建完成后被立即调用。在这个阶段,实例已经完成了数据观测、属性和方法的运算,但是 DOM 元素还未生成,不能进行 DOM 操作。
-
beforeMount: 在挂载开始之前被调用,相关的 render 函数首次被调用。在这个阶段,Vue 已经将模板编译成 render 函数,但是虚拟 DOM 还未生成。
-
mounted: el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子。在这个阶段,实例已经完成了挂载,可以访问到组件渲染后的 DOM 元素,可以进行 DOM 操作。
-
beforeUpdate: 在数据更新之前调用,发生在虚拟 DOM 重新渲染和打补丁之前。在这个阶段,虚拟 DOM 和 DOM 还未更新,此时可以获取更新前的状态。
-
updated: 在数据更新之后调用,发生在虚拟 DOM 重新渲染和打补丁之后。在这个阶段,DOM 已经更新完成,可以执行一些操作,如获取更新后的状态或操作 DOM。
-
beforeDestroy: 在实例销毁之前调用。在这个阶段,实例仍然完全可用,可以执行一些清理操作,如清除定时器、取消订阅等。
-
destroyed: 在实例销毁后调用。该钩子被调用时,Vue 实例的所有指令和事件监听器都已被解绑,组件也已被销毁,无法再访问组件的属性和方法。
这些生命周期函数允许你在组件的不同阶段执行特定的操作,例如初始化数据、调用接口获取数据、渲染 DOM、清理定时器、取消订阅等。通过合理地使用这些生命周期函数,可以更好地管理组件的生命周期,提高组件的性能和可维护性。
3. 每个生命周期函数可以做什么?
在 Vue.js 组件中,有一系列的生命周期钩子函数,它们允许你在组件的不同阶段执行代码。以下是 Vue.js 2.x 中常见的生命周期函数及其用途:
-
beforeCreate:
- 在实例初始化之后,数据观测 (data observer) 和 event/watcher 事件配置之前被调用。
- 通常在此阶段进行一些初始化操作,例如数据的准备工作。
-
created:
- 在实例创建完成后被立即调用。
- 通常在此阶段进行一些异步操作,例如获取数据、设置定时器、发送网络请求等。
-
beforeMount:
- 在挂载开始之前被调用:相关的 render 函数首次被调用。
- 通常在此阶段进行一些操作,例如修改 DOM。
-
mounted:
- el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子。
- 通常在此阶段进行一些 DOM 操作,例如调用第三方库、初始化组件等。
-
beforeUpdate:
- 在数据更新之前调用,发生在虚拟 DOM 重新渲染和打补丁之前。
- 通常在此阶段进行一些操作,例如获取更新前的 DOM 状态。
-
updated:
- 在数据更新之后调用,发生在虚拟 DOM 重新渲染和打补丁之后。
- 通常在此阶段进行一些操作,例如对更新后的 DOM 进行操作,或者调用第三方库。
-
beforeDestroy:
- 在实例销毁之前调用。在这一步,实例仍然完全可用。
- 通常在此阶段进行一些清理操作,例如清除定时器、取消订阅等。
-
destroyed:
- 在实例销毁后调用。该钩子被调用时,Vue 实例的所有指令和事件监听器都已被解绑。
- 通常在此阶段进行一些最终的清理工作,例如释放内存、解绑事件等。
以上是 Vue.js 组件生命周期的主要钩子函数及其用途。通过合理地使用这些生命周期函数,你可以在组件的不同阶段执行特定的操作,以满足项目的需求,并管理组件的生命周期。
3. 求两个单链表的公共节点
要找到两个单链表的公共节点,可以采用以下步骤:
- 获取链表长度: 遍历两个链表,分别获取它们的长度。
- 将长链表指针先移动到与短链表相同长度的位置: 若链表长度不等,将长链表的指针向后移动(长链表长度 - 短链表长度)个节点,使得长链表和短链表从同一起点开始比较。
- 同步移动两个链表指针: 从相同位置开始,同时移动两个链表的指针,直到找到第一个相同的节点,或者遍历到链表末尾。
- 返回公共节点或空: 如果找到了相同的节点,则返回该节点;如果遍历完两个链表都没有找到相同的节点,则说明两个链表没有公共节点,返回空。
下面是一个简单的 JavaScript 实现:
class ListNode {
constructor(val, next = null) {
this.val = val;
this.next = next;
}
}
const getIntersectionNode = function(headA, headB) {
if (!headA || !headB) {
return null; // 若有一个链表为空,则直接返回 null
}
// 获取链表长度
let lenA = getLength(headA);
let lenB = getLength(headB);
// 将长链表指针移动到与短链表相同长度的位置
let pA = headA;
let pB = headB;
if (lenA > lenB) {
for (let i = 0; i < lenA - lenB; i++) {
pA = pA.next;
}
} else if (lenA < lenB) {
for (let i = 0; i < lenB - lenA; i++) {
pB = pB.next;
}
}
// 同步移动两个链表指针,找到第一个相同的节点
while (pA !== null && pB !== null && pA !== pB) {
pA = pA.next;
pB = pB.next;
}
return pA; // 返回公共节点或 null
// 辅助函数:获取链表长度
function getLength(head) {
let len = 0;
let p = head;
while (p !== null) {
len++;
p = p.next;
}
return len;
}
};
这段代码首先计算两个链表的长度,然后将较长的链表指针移动到与较短链表相同长度的位置,最后同步移动两个链表指针,直到找到第一个相同的节点或者遍历到链表末尾。
3. Promise.all和race,以及输入数组为[],或者没有输入的时候,错误处理?
Promise.all 和 Promise.race 是 Promise 提供的两个静态方法,用于处理多个 Promise 实例的并发执行。
-
Promise.all:- 接收一个可迭代的对象(通常是数组),里面的每个元素都是一个
Promise实例。 - 当所有
Promise实例都成功完成时,返回一个新的Promise实例,其状态为成功,值是所有Promise实例成功返回值组成的数组。 - 如果其中任何一个
Promise实例失败(reject),则立即返回一个失败状态的Promise实例,其原因是第一个失败的Promise实例的原因。
- 接收一个可迭代的对象(通常是数组),里面的每个元素都是一个
-
Promise.race:- 接收一个可迭代的对象(通常是数组),里面的每个元素都是一个
Promise实例。 - 返回一个新的
Promise实例,其状态和值取决于第一个完成的Promise实例。 - 如果其中任何一个
Promise实例成功(resolve)或失败(reject),则立即返回一个与之相同状态的Promise实例。
- 接收一个可迭代的对象(通常是数组),里面的每个元素都是一个
对于错误处理:
- 当输入数组为空数组
[]时,Promise.all会直接返回一个成功状态的Promise实例,其值是一个空数组[]。 - 当输入数组没有元素时,
Promise.all也会直接返回一个成功状态的Promise实例,其值是一个空数组[]。 - 对于
Promise.race,同样适用以上规则。
需要注意的是,如果输入数组中存在非 Promise 实例的元素,那么它们会被转换成 Promise 实例,并且立即返回成功状态,其值为该元素。如果有任何一个非 Promise 元素转换失败(例如不可转换为 Promise 实例),则 Promise.all 会立即返回一个失败状态的 Promise 实例,其原因是第一个转换失败的元素的原因。
- 反问:技术栈,部门hc
面试反馈:我的能力去一个二线互联网没问题,呜呜呜是我不配了
阿里 银泰一面
- 自我介绍
- 项目的详细介绍
- 防抖节流的区别
防抖(Debounce)和节流(Throttle)是两种常用的优化技术,用于减少高频率事件(如滚动、resize、输入等)的触发次数,提高性能和用户体验。它们的区别在于触发的时间处理方式不同:
-
防抖(Debounce): 在事件被触发后,等待一段时间(例如 100ms),如果在这段时间内没有再次触发该事件,才执行事件处理函数。如果在等待时间内再次触发了事件,则重新计时。防抖适用于需要等待一段时间后执行最后一次操作的场景,例如输入框输入时的搜索功能,用户在连续输入时不会频繁触发搜索请求,而是在停止输入后才触发搜索请求。
-
节流(Throttle): 在事件被触发后,以固定的时间间隔(例如 100ms)执行事件处理函数,不管这段时间内事件触发了多少次。节流适用于需要在一段时间内最多执行一次操作的场景,例如滚动页面时的图片懒加载,可以在用户滚动页面时每隔一段时间检查一次图片是否需要加载,而不是每次滚动都检查。
简而言之,防抖是等待一段时间后执行最后一次操作,节流是每隔一段时间执行一次操作。两者都可以有效减少事件触发的频率,提高性能,但适用于不同的场景。
- 聊项目
- 反问
阿里 银泰二面 目前还没挂
1. Vue和React的区别?
Vue 和 React 是当前最流行的前端框架之一,它们有一些相似之处,但也有很多不同之处。以下是它们的主要区别:
-
设计理念:
- Vue:Vue 被设计为渐进式框架,可以逐步应用到项目中,甚至只用于单个页面或组件。Vue 的核心思想是通过简单的 API 和响应式数据绑定来构建用户界面。
- React:React 是一个用于构建大型应用的 JavaScript 库,专注于组件化和虚拟DOM。它提供了一种声明式的方式来描述用户界面,并提供了组件化的开发模式。
-
模板语法:
- Vue:Vue 使用模板语法,类似于 HTML,将数据绑定到 DOM 上。模板中包含指令、插值表达式等,使得编写模板更加直观易懂。
- React:React 使用 JSX(JavaScript XML)语法,允许在 JavaScript 代码中直接编写类似于 HTML 的标记结构。JSX 提供了更直接的组件描述方式,但需要编译器将 JSX 转换为普通的 JavaScript。
-
组件化:
- Vue:Vue 提供了非常简单的组件化方案,允许将应用拆分为多个可重用的组件。Vue 组件可以包含模板、脚本和样式,并提供了丰富的组件通信和组合方式。
- React:React 也支持组件化开发,但更强调函数式组件和组件之间的数据流。React 组件通常只包含 render 方法,数据通过 props 和 state 进行传递。
-
状态管理:
- Vue:Vue 提供了 Vuex 这样的状态管理库,用于管理应用中的状态。Vuex 集中管理所有组件的状态,并提供了丰富的工具和 API 来处理状态变化。
- React:React 通常使用 Context API 和第三方库(如 Redux)来管理应用的状态。Context API 允许跨组件层级传递数据,而 Redux 提供了可预测的状态管理方案。
-
生态系统:
- Vue:Vue 生态系统相对较小,但也有许多官方和第三方的插件、库和工具,如 Vue Router、Vuex、Vue CLI 等。
- React:React 生态系统更加庞大和成熟,有大量的社区支持和第三方库可供选择,如 React Router、Redux、Next.js 等。
总的来说,Vue 更注重开发体验和灵活性,适合快速开发小型到中型的应用;而 React 更注重性能和扩展性,适合构建大型、复杂的应用和跨平台应用。选择使用哪个框架取决于项目的需求、团队的技术栈和个人偏好。
- 在现在的技术背景下,前端更新迭代很迅速,请问作为前端工程师你应该怎么保持核心竞争力?
4. 方案设计:如何实现安全的登录?
实现安全的登录是保障用户账户信息不被盗用和泄露的关键。以下是一些方案设计的建议:
-
使用HTTPS协议: 确保登录页面和用户提交的信息在传输过程中经过加密,防止信息被中间人窃取或篡改。
-
密码加密存储: 在服务器端存储用户密码时,应采用加密算法对密码进行加密存储,例如使用哈希函数加盐(salted hash)存储用户密码,防止密码泄露后被破解。
-
密码策略: 强制用户设置复杂度较高的密码,包括字母、数字和特殊字符,并且密码长度应足够长,以增加密码破解的难度。同时,建议定期要求用户更改密码,避免长期使用相同的密码。
-
多因素身份验证(MFA): 引入多因素身份验证机制,除了传统的用户名和密码登录外,还可以要求用户输入手机验证码、邮箱验证码、指纹识别等其他因素,提高账户安全性。
-
防止暴力破解: 限制用户登录失败次数,并采取验证码、延迟响应等措施来防止暴力破解登录密码。
-
安全的会话管理: 在用户登录成功后,通过安全的会话管理机制来管理用户的登录状态,确保会话标识符的安全性,防止会话劫持和会话固定等攻击。
-
安全的密码找回流程: 提供安全的密码找回流程,例如通过邮箱验证或手机验证来重置密码,确保只有合法用户才能重置密码。
-
定期安全审计: 定期对登录系统进行安全审计,包括漏洞扫描、安全代码审查等,及时发现和修复潜在的安全风险。
综上所述,实现安全的登录需要综合考虑传输安全、密码存储安全、身份验证安全、会话管理安全等方面,采取多种措施保障用户账户信息的安全性。
3. 方案设计:类似微博,触底加载,每次加载数据的时候,能无重复的加载数据以及对新插入的内容也能进行加载;而且每个人每个人看到的内容都不一样,如何保证
实现类似微博的触底加载,并保证每次加载数据时无重复且能加载新插入的内容,以及确保每个用户看到的内容都不一样,可以考虑以下方案设计:
-
分页加载: 将数据按页分割,每次加载一页数据,当用户触底时加载下一页数据。这样可以避免一次性加载过多数据,提高性能。同时,每页数据的大小应适当,不宜过大。
-
数据去重: 在加载数据时,通过标记已加载过的数据,避免重复加载。可以通过记录已加载数据的ID或其他唯一标识来实现去重。
-
排序策略: 确保新插入的内容也能被加载,可以在数据的存储和加载过程中使用合适的排序策略。例如,可以按时间戳或其他标识对数据进行排序,确保新插入的内容能够被及时加载。
-
个性化推荐: 根据用户的偏好和行为,提供个性化的内容推荐。可以根据用户的历史浏览记录、点赞、评论等行为,采用推荐算法为用户推荐感兴趣的内容,从而确保每个用户看到的内容都不一样。
-
缓存策略: 使用适当的缓存策略来提高数据加载的效率和用户体验。可以在客户端和服务器端都使用缓存,减少重复请求和加快数据加载速度。
-
实时更新: 确保数据的实时更新,及时推送新插入的内容给用户。可以使用WebSocket等实时通信技术,将新数据推送给在线用户,从而保持用户看到的内容的实时性。
综上所述,通过合理的分页加载、数据去重、排序策略、个性化推荐、缓存策略和实时更新等手段,可以实现类似微博的触底加载,并保证每次加载数据时无重复、能加载新插入的内容,同时确保每个用户看到的内容都不一样。
-
如何使用两个队列实现一个栈 要使用两个队列实现一个栈,可以采用以下方法:
-
使用队列A和队列B两个队列来实现栈的操作。
-
入栈操作时,将元素直接放入队列A中。
-
出栈操作时,先将队列A中的元素依次出队并入队到队列B中,直到剩下最后一个元素,然后将该元素出队,即完成出栈操作。
-
为了保持栈的结构,再将队列B中的元素依次出队并入队到队列A中,即完成了出栈操作。
-
这样就实现了用两个队列模拟栈的操作,其中一个队列用于入栈和出栈操作,另一个队列用于辅助出栈操作。
下面是一个使用两个队列实现栈的示例代码(假设队列的实现已经存在):
class Stack {
constructor() {
this.queueA = []; // 队列A用于入栈和出栈操作
this.queueB = []; // 队列B用于辅助出栈操作
}
push(element) {
this.queueA.push(element); // 元素直接入队到队列A
}
pop() {
if (this.isEmpty()) {
return null; // 栈为空,返回null
}
// 将队列A中的元素依次出队并入队到队列B中,直到剩下最后一个元素
while (this.queueA.length > 1) {
this.queueB.push(this.queueA.shift());
}
// 出队最后一个元素,即完成出栈操作
const poppedElement = this.queueA.shift();
// 将队列B中的元素依次出队并入队到队列A中,保持栈的结构
while (this.queueB.length > 0) {
this.queueA.push(this.queueB.shift());
}
return poppedElement;
}
isEmpty() {
return this.queueA.length === 0;
}
}
// 测试
const stack = new Stack();
stack.push(1);
stack.push(2);
stack.push(3);
console.log(stack.pop()); // 输出 3
console.log(stack.pop()); // 输出 2
console.log(stack.pop()); // 输出 1
console.log(stack.pop()); // 输出 null(栈为空)
这个示例中,我们通过两个队列模拟了栈的入栈和出栈操作,并保持了栈的特性。
3. 1000人里有一个阳性,如何快速找到?---二分
在有1000个人的情况下,我们可以使用二分法来快速找到这一个阳性人士。二分法是一种查找算法,适用于有序列表或数组中的元素。在这个问题中,我们可以将1000个人的编号从1到1000按顺序排列,然后开始二分查找。
具体步骤如下:
-
初始化搜索范围为1到1000。
-
将搜索范围的中间编号与阳性人士的编号进行比较。
-
如果中间编号小于阳性人士的编号,则更新搜索范围为中间编号到1000。
-
如果中间编号大于阳性人士的编号,则更新搜索范围为1到中间编号。
-
不断重复步骤2到4,直到找到阳性人士的编号。
下面是一个使用 JavaScript 实现二分查找的示例代码:
function findPositivePerson() {
let left = 1;
let right = 1000;
while (left < right) {
let mid = Math.floor((left + right) / 2);
// 这里假设阳性人士的编号为500
if (mid < 500) {
left = mid + 1;
} else {
right = mid;
}
}
return left; // 找到阳性人士的编号
}
console.log("阳性人士的编号是:" + findPositivePerson());
在这个示例中,假设阳性人士的编号为500,通过二分查找,我们可以快速找到这个阳性人士的编号为500。二分查找的时间复杂度为O(log n),因此可以在很短的时间内找到目标。
-
最近在学什么新技术吗?
-
chatgpt对前端有什么冲击?他不能代替前端,怎么说?哪些地方不如前端工程师?
-
反问环节
作者:钱多多哦耶
链接:www.nowcoder.com/discuss/541…
来源:牛客网