前端知识

  • 接下来的内容会是我 面试 / 遇到 不会的点,或者是掌握得不清楚的点,我会尽可能将整个流程写明白,让我自己能跑通整个流程。

浏览器的同源策略?

What

  • 当我们写好一个后端程序和前端程序的时候,前端向后端发送请求,提示CORS未通过,这通常是浏览器的同源策略导致的。
  • 同源策略是浏览器对跨源读写的限制,例子:js不能读另一个源的响应内容。
  • 链路:浏览器先执行同源策略 – 如果跨源,则需要带上CORS响应头显式访问 – 否则浏览器拦截

    同源策略是拦截策略,而CORS是服务器给浏览器的许可证明

Why

  • 这是为了防止Web里的跨源攻击,运行在某个 origin 下的脚本,不能读取其他 origin 的存储(localStorage/cookie/DOM)和响应内容;哪怕这些请求是用户本人在同一个浏览器里触发的。

How

  • 服务端通过 CORS 配置决定向哪些 Origin 返回允许头;若未允许,浏览器会拦截前端 JS 读取响应(即使请求到达并拿到 200)。
  • 校验三大部分:协议、主机名、端口
1
2
3
4
5
6
7
8
9
10
11
12
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("http://localhost:5173"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
config.setAllowedHeaders(List.of("*"));
config.setAllowCredentials(true);
config.setMaxAge(3600L);

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}

XSS攻击?

What

  • XSS攻击是用户将一段脚本(js)代码伪装成评论插入到系统里,当别人展开评论的时候,这段脚本会在网站里执行(因为当你展开评论的时候,浏览器把用户输入当 HTML 解析,前端可能会将评论使用innerHTML插入进到页面里,脚本被执行)
    • 用户可能会看到奇奇怪怪的弹窗或者跳转到别的页面
    • 账号被莫名改资料
  • 如果你是权限比较高的用户,执行了恶意脚本,很可能就会让系统崩溃。

How to prevent

  • 核心原则永远不要把用户的输入当成HTML渲染
    • React默认渲染字符串是转义的,{content}
    • 坑:如果为了支持富文本(图片,代码,换行)使用了dangerouslySetInnerHTML,这样就会插入脚本。
  • 解决方法:
    1. 输出编码要默认转义,用纯文本来渲染,不允许用html
    2. 白名单清洗:如果必须要富文本,过滤掉事件脚本,事件属性,危险的url结构
    3. token尽量不要放在localstorage里,可以放在HttpOnly cookie,这样js读不到cookie
    4. 设置SameSite,有三种模式,strict,lax以及none,分别控制跨站是否自动带上cookie
  • 解决markdown富文本的XSS攻击策略
    1. 设置白名单,只允许我们可以的标签,比如p,a,strong,code等,对属性也要白名单,禁止所有的事件属性,比如on*
    2. 禁止inline script
    3. 代码块永远escape(转义)后放进code和pre里展示

JWT更安全的登录?双Token

What

  • 我发现了原来的逻辑是改Localstorage就可以让前端渲染出管理员的界面,这样不行
  • 于是引入了JWT的accessToken以及refreshToken
    • accessToken一般时间短,15-30分钟,存放在React的内存里,一刷新就没有
    • refreshToken时间在7-30天,存放在httponly的cookie里
    • 此外这个refreshToken还存放在了redis里,使用版本号控制,让非法用户强制下线

How

登录功能

  • 用户首次登陆的时候后端校验账户和密码是否合法,如果合法就签发两个token
  • access token 过期 → 前端请求业务接口返回 401
  • 前端调用 /api/v1/auth/refresh(不带 refresh token,浏览器会自动附带 HttpOnly Cookie)
  • 后端从 Cookie 里拿 refresh token,去 Redis 校验是否存在/是否有效
  • 校验通过 → 生成新 access token(和新的 refresh token),并在 Redis 里更新
  • 返回新的 access token 给前端,继续请求

    来回时间就是一个rtt

踢人功能

  • 在redis里添加tokenVersion,因为我们登陆的时候用户还是有当前的accessToken,所以他还能存活这么久
    • 加一个user:tokenVersion:<uid>在redis里,签发的时候把ver写进JWT,
    • 踢人就增加版本号,这样让他的所有旧accessToken失效。原因是我们的在redis里的逻辑是 user:tokenVersion:<uid> = N,拦截器校验时比较Redis ver,如果不一致就401

既然有了踢人功能,那能不能把accessToken设置为24h?

  • 不建议
    1. jwt最大的优点就是无状态,后端只需要使用cpu进行简单的数学运算就可以得到结果
    2. 如果是24h,那用户每一次调用接口的时候都会查询redis,那本质上和session没有区别,失去了 JWT 无状态校验的性能优势。
    3. 缩短风险窗口: 短时效(15-30min)的 AT 配合长时效的 RT,可以在‘不需要频繁查库’和‘用户被盗号后的损失’之间取得平衡。即便不实时查 Redis,黑客的攻击窗口也仅限于这几分钟。

项目登录流转

How

  1. 用户点击页面触发接口请求前端,拦截器从localstoragehnu_token,放到请求头 Authorization: Bearer xxx
  2. 后端从 Authorization 里取Bearer token解析JWT,再校验token是否一致
  3. token过期/无效时,接口会返回401,前端响应拦截器会调用refresh的api,这个请求会自动带上httpOnly的 hnu_refresh_token的cookie
  4. refresh成功后,后端返回新的accessToken,前端setToken()写回localStorage,然后重放刚刚失效的请求。
  5. 为什么刷新页面登录态不掉,因为不是只放内存:accessToken 持久化在 localStorage。页面刷新后,AuthProvider 初始化会重新读 localStorage,并调用 /api/v1/users/me 做一次有效性校验。

    refresh token 为什么放 Cookie,不放前端 localStorage?
    答:refresh token 通过 HttpOnly Cookie 下发,前端 JS 拿不到,减少 XSS 窃取风险;access token 才放 localStorage 走请求头。
    “每次请求都带 Bearer accessToken,后端拦截器验 JWT + Redis 版本;401 时用 cookie 里的 refreshToken 换新 token;刷新页面靠 localStorage 恢复,所以不会直接掉线。”

项目路由管理

  1. 公开路由
  • /login、/register、/、/posts/:id 直接可访问。
  1. 登录保护路由
  • 我封装了 RequireAuth,基于 useAuth() 的 token + loading 判断。
  • 没登录就 Navigate 到 /login。
  1. 管理员路由
  • 我再封装 RequireAdmin,在登录基础上再判断 user.role === ‘ADMIN’,否则跳回首页。

浏览器的事件循环机制是什么?

What

  • 事件循环是浏览器在单线程上运行 JS 的调度机制:把“同步 JS 执行”与“外部事件/异步结果(定时器、网络、交互等)”通过任务队列串起来,循环取任务执行。
  • 由三部分组成:调用栈、Web APIS、任务队列
1
2
3
4
5
6
7
setTimeout(()=>{
console.log(1);

},0)
Promise.resolve().then(()=>{
console.log(2);
})

会打印2,1。是因为这一段script是一个宏任务,然后setTimeout先是注册了一个定时器任务,把这个回调函数log1放到了宏任务队列里,就到微任务,微任务将这一段script的宏任务收尾,所以先log,然后清空了微任务之后。script 这个宏任务已经执行完了;之后从宏任务队列里取的是 定时器回调这个新的宏任务。

  • 宏任务:整段Script,setTimeout\setInterval回调,DOM事件回调(Click,Input)
  • 微任务:Promise

Why

  • js是单线程的,同一时间只能做一件事,避免多线程共享DOM带来的竞态
  • 让页面响应可控,长任务不会永远占住

How

  • “执行一个宏任务 → 然后清空微任务队列 →(必要时渲染)→ 下一轮宏任务”。
  • 执行的顺序
    • 宏任务:定时器(setTimeout,setInterval)回调、点击回调、整段Script
    • 微任务:Promise.then、catch/finally用于“本轮宏任务的收尾/延续”
  • 如果浏览器一直执行宏任务?
    • 问题:理论上来说因为是单线程,宏任务如果一直不返回(同步死循环/超长计算),浏览器确实会一直卡在这个宏任务里。
    • 解决方法
      • 把大任务分成多个短任务
      • 时间分片
      • 给Worker
  • 在浏览器的事件循环标准中,UI 渲染通常发生在所有微任务(Microtasks)执行完毕之后,以及下一个宏任务(Macrotask)开始之前。

节流和防抖有什么区别?

What

  • 防抖(debounce):在连续触发中不断重置计时器,停止触发一段时间后才执行一次(“取最后一次”)。
  • 节流(throttle):在连续触发中按固定间隔执行,单位时间最多执行一次(“限速”)。

How

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function debounce(fn,wait = 300) {
let timer = null
return function(...args) {
if (timer) {
clearTimeout(timer)}
timer = setTimeout(()=>{
fn.apply(this,args)
},wait)
}
}

function throttle(fn,wait = 200) {
let last = 0
return function (...args) {
const now = Date.now()
if (now - last >= wait) {
last = now
fn.apply(this,args)
}
}
}

Why

  • 防抖:解决“抖动触发导致的重复计算/重复请求”,减少无效开销(如输入联想、频繁点击)。
  • 节流:解决“持续高频触发导致的性能压力/掉帧”,让回调执行频率可控(如滚动、鼠标移动、拖拽)。

    为什么要return function,因为function是一个函数,需要返回值,这里的返回值就是一个函数

遍历找到D盘的两个重复文件?

How

  • 文件夹有递归嵌套的结构,每一个文件夹就像是树一样,遍历树就使用DFS/BFS/递归
  • 构建 Map<size, filePaths[]>,只保留 filePaths.length >= 2 的桶作为候选(不同 size 必不相同)。
  • 对候选文件用流式读取计算内容 hash(如 SHA-256/BLAKE3),构建 Map<hash, filePaths[]>;同一 hash 下的文件认为“高度可能相同”。

深拷贝和浅拷贝

What

  • 浅拷贝:只复制一层属性;若属性值是对象/数组/函数等引用类型,复制的是引用,因此会共享同一份深层数据。
  • 深拷贝:递归复制各层级,让嵌套对象/数组变成新的引用,通常不共享深层引用。

    Object.create 不是拷贝,是“以某对象作为原型”创建新对象。

Why

  • React/Redux 等依赖不可变更新 + 浅比较(===)做渲染与 memo 判断:你只需要“沿修改路径拷贝”(结构共享),而不是全量深拷贝。
  • 全量深拷贝会带来:CPU/内存开销、GC 压力、缓存失效(引用每次都变,memo 失效)。

How

深拷贝

1
2
3
//方法1:使用structuredClone
const p = { name: 'lala', age: 18, arr: [1,2,3] }
let s = structuredClone(p)

浅拷贝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//方法1:展开运算符
const p = { name: 'lala', age: 18, arr: [1,2,3] }
let s = {...p}

//方法2:使用Object的assign方法
Demo
const target = { a: 1, b: 2 };
const source = { b: 4, c: 5 };

const returnedTarget = Object.assign(target, source);

console.log(target);
// Expected output: Object { a: 1, b: 4, c: 5 }

console.log(returnedTarget === target);
// Expected output: true

s = Object.assign({},p)

对React的理解

What

  • JSX 不是 HTML,它会被编译成 React.createElement(...) 的调用,产出的是一棵React Element 树(普通 JS 对象描述 UI)。它是“UI 的描述”,不是浏览器 DOM。
  • React 以组件为单位,由 state/props 驱动渲染:每次 render 本质是“根据当前 state/props 计算下一棵 element 树”。

How

  • state/props 变化时,React 会重新执行组件函数得到 next element tree,再和 prev element tree 做 reconciliation(协调/对比),决定哪些地方需要更新。

所以React是一个声明式的UI框架,依托于Props/State,JSX会被编译为createElement,得到一个Element树,当状态变化时就会render一颗新树,然后通过diff算法来比较计算最小开销,最后把差异提交到真实的DOM

Bad

虚拟DOM也有一些不好的地方,它不是免费的抽象,在一些场景里会引入额外成本/复杂度。

  1. 额外的计算与内存开销:每次更新 React 都要重新执行组件、生成 element/fiber、做 reconciliation,再 commit。对超高频更新(动画、拖拽、实时渲染)可能比“直接命令式改 DOM/Canvas/WebGL”更重。
  2. 启发式 diff 不是全局最优:React 的对比依赖规则(同层、同类型、key),不是最小编辑距离;key 写错/用 index 会导致错误复用、状态串位或不必要重建,性能和正确性都受影响。
  3. 误用会导致性能问题:不稳定引用、无意义 re-render、巨型列表不做虚拟滚动、在 render 里做重计算——这些都会放大虚拟 DOM 的成本,所以工程上常配 memo/useMemo/useCallback、列表虚拟化、拆分组件。

useMemo和useCallback是什么?(react优化相关)

What

  • useMemo:记住“计算出来的值”(数组/对象/数字都行),依赖不变就复用上次结果。
  • useCallback:记住“函数本身”(保持函数引用稳定)。它等价于:

    useCallback(fn, deps) ≈ useMemo(() => fn, deps) 它们都不会阻止组件 re-render;组件仍会 render,只是你拿到的值/函数不重新创建。就类似于,你唯一的输入一定会得到唯一的输出

How

1
2
3
4
5
const list = useMemo(() => expensiveFilter(data, keyword), [data, keyword])

const onSend = useCallback(() => {
sendMessage(text)
}, [text])

Why

  • 省计算:过滤/排序/聚合很贵时,用 useMemo 避免每次 render 重算。
  • 稳定引用,配合浅比较优化:用 useMemo/useCallback 让引用稳定,避免“不必要的触发”。

    “数组/对象/函数每次 render 都会创建新引用;如果它们作为 props/依赖,会让浅比较认为变了,从而触发不必要更新。useMemo/useCallback 用来稳定这些引用。”

浅比较

  • 对基本类型(Number,Boolean,String):比 值(===)
  • 对引用类型(arr[],obj{},function()):比 引用地址(===),不看里面内容。
1
2
3
4
5
6
7
8
9
10
const a = { x: 1 }
const b = { x: 1 }
a === b // false(内容一样,但不是同一个引用)

const c = a
a === c // true(同一个引用)

const arr1 = [1,2]
const arr2 = [1,2]
arr1 === arr2 // false

在 React 里常见浅比较场景:

  • React.memo 默认用浅比较 props:只要某个 prop 引用变了,就认为“变了”。
  • useEffect/useMemo/useCallback 的依赖数组比较:每一项用 Object.is(和 === 很像)做浅比较。

数组怎么合并?

How

1
2
3
4
5
6
//方法1:concat拼接
let a = [1,2,3]
let b = [4,5,6]
let c = [].concat(a,b)
//方法2:展开运算符
let c = [...a,...b]

如何写一个间隔一秒自动切换颜色的正方形

How

  • 间隔:setInterval
  • 自动切换 <==> 状态监听 <==> 使用hook(useState)
  • 清除定时器 <==> useEffect,需要返回清除函数
  • react里传样式传的是一个JSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { useEffect, useState } from "react"

function App() {
const color = ['red','green','pink']
const [col,setCol] = useState(0)

useEffect(()=>{
const timer = setInterval(()=>{
setCol((prev) => (prev + 1) % color.length)
},1000)
return ()=>clearInterval(timer)
},[])
return (
<div>
<div style={{ width: 100, height: 100, backgroundColor: color[col] }} />
</div>
);
}

export default App

useEffect是什么?

What

  • useEffect 是 React 的“副作用hook”:让你在组件渲染到页面之后去做一些事,并且能在需要时清理这些事。
  • 副作用
    • 定时器 setInterval/setTimeout
    • 订阅/事件监听 addEventListener
    • 发请求 fetch/axios
    • 操作 DOM、设置标题 document.title = …

      这些都不应该放在组件函数体里反复执行。

1
2
3
4
5
6
useEffect(() => {
// 这里:做副作用
return () => {
// 这里:清理副作用
};
}, [deps]);

How

依赖数组 [deps] 决定“执行频率”

  • 不写第二个参数:每次渲染后都执行(很少用,容易反复绑定/请求)
  • 写 []:只在组件首次挂载后执行一次(适合建定时器/订阅一次)
  • 写 [a, b]:a 或 b 变化时执行(常用于“根据某个 state/prop 变化去请求/同步”)

return 的清理函数什么时候执行

  • 组件卸载时执行一次
  • 或者依赖变化导致 effect 重新执行时:先清理上一次,再执行新的

内存泄漏有哪些场景?

Where

  1. 循环依赖,A依赖B, B依赖A
  2. 定时器/轮询没有清理 (useEffect return 一个clearInterval)
  3. 闭包长期持有对象

数组有哪些遍历方法?

  1. 正常的for循环
  2. forEach:用于执行回调函数,但是一般不修改原数组内容,用于打印值
1
2
3
4
5
6
7
let arr = [1,2,3]
//打印
arr.forEach((x)=>console.log(x))
//执行函数
arr.forEach((x)=>console.log(x*x))
//修改值
arr.forEach((x,i)=>arr[i] = x*x)
  1. map: 用于创建一个新的数组,本质也是遍历
1
2
3
let arr = [1,4,9]
let b = arr.map((x)=>x*x*x)
console.log(b);
  1. for … of / for…in 用于遍历值

对Promise的理解

Why

  • 避免回调地狱的出现,在没有promise之前,需要很多个if else来列举出全部的情况,现在只需要成功或失败
  • Promise 把异步结果抽象成一个对象,提供统一的组合方式(then 链、catch、all 等),让控制流更清晰。

What

  • Promise是未来承诺会有返回值,然后去执行对应的逻辑
  • 三个状态:pending、fulfilled、rejected。状态不可逆
  • then去执行fulfilled的回调,catch去执行rejected的回调

How

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const p = new Promise((resolve,reject)=>{
if (true) {
resolve('ok')
}
else {
reject(new Error('error'))
}
})
p.then((data)=>{
console.log(data)
})
.catch((err)=>{
console.log(err)
})

两个API

  • Promise.all: 所有成功才会走then,如果有一个失败了,就会报错;
1
2
3
4
5
6
7
8
9
const p1 = Promise.resolve('data1')
const p2 = Promise.resolve('data2')
const p3 = Promise.reject('data3')

Promise.all([p1,p2])
.then((res)=>console.log(res))
.catch((err)=>console.log(err))

[ 'data1', 'data2' ]
  • Promise.allSettled: 为什么会出现他,如果我们主页有四个图表,肯定不想因为某一个挂了就不渲染其他正常的地方,allSettled只会返回一个then,附上它的status
1
2
3
4
5
6
7
8
9
10
11
const p1 = Promise.resolve('成功')
const p3 = Promise.reject('网络延迟')

Promise.allSettled([p1,p3])
.then((res)=>console.log(res))
.catch((err)=>console.log(err))

[
{ status: 'fulfilled', value: '成功' },
{ status: 'rejected', reason: '网络延迟' }
]

对async和await的理解

What

  • async 保证函数返回 Promise;return x 等价 Promise.resolve(x),throw err 等价 Promise.reject(err);await 会等待一个 Promise,成功返回值,失败抛异常。

Why

  • asyncawaitPromise的语法糖,将.then拍平,用try/catch包裹promise的执行结果
  • async function f(){}:保证这个函数返回 Promise
  • 在 async 里 return x,等价于 return Promise.resolve(x)
  • 在 async 里 throw err,等价于 return Promise.reject(err)
  • await 后面接受任何值,但如果是 Promise 才会“等待”;不是 Promise 就立刻得到该值(等价于 await Promise.resolve(x))。这个async函数里就需要await后面的Promise对象的状态,如果是fulfilled,走try里面的逻辑,如果是rejected,走catch里的逻辑

How

1
2
3
4
5
6
7
8
9
async function requestJSON(url) {
const res = await fetch(url); // await 等网络返回 Response
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json(); // await 等 body 解析成 JSON
}

requestJSON("https://api.github.com/users/octocat")
.then(console.log)
.catch(e => console.log("err:", e.message));

受控组件和非受控组件

What

  • 受控组件(Controlled):表单值 由 React state 控制。value/checked 绑定 state,onChange 更新 state。单一数据源:state。
  • 非受控组件(Uncontrolled):表单值DOM 自己维护。React 不用 state 绑住值,需要时用 ref 读取(或表单提交时读)。数据源:DOM。

浏览器存数据有哪些方式? localStorage和sessionStorage有什么区别

What

  • Cookie:最早的存储方式,容量小(约 4KB),每次请求都会带在 HTTP 头部,主要用于身份验证(Session ID)。
  • Web Storage:包含 localStorage 和 sessionStorage。
  • IndexedDB:浏览器内置的“非关系型数据库”,可以存储大量结构化数据(甚至是文件和二进制数据),且支持索引和事务。
  • Cache API:主要用于 Service Worker,用来缓存网络资源(如 HTML、JS、图片),实现离线访问。

Why

  • localStorage
    • 永久有效。除非你手动删除或用户清理浏览器缓存,否则关闭浏览器后数据依然存在。
    • 同源共享。同一域名下的所有页面、标签页都可以读写同一个 localStorage。
  • sessionStorage
    • 会话级有效。一旦标签页(Tab)或窗口被关闭,数据就会被清空。
    • 标签页独立。即使是同一个网站,在不同标签页打开,它们的 sessionStorage 也是互不干扰的。

状态码问题

如果前端报404该如何排查?

  1. 确认请求地址:看看url是不是拼错了
  2. 查看方法是不是请求错了:如果接口是post,但是使用了get
  3. 确认前端代理:nginx或者next.config是不是正确代理到了api
  4. 确认后端接口是否存在
  5. 路径部署可能错误

全栈开发遇到500如何排查?

  1. 500几乎一定是服务端问题,先看日志或者控制台
  2. postman/curl去直接调接口
  3. 检查请求参数列表:JSON格式,字段名,类型
  4. 代码逻辑错误
  5. 中间件炸了或者没开:SQL、Redis、第三方api

评论如果有很多的话,如何处理?

  • 作为后端
    • 设置分页查询,将热门的评论放到redis中
  • 作为前端
    • 虚拟列表

      What

      • 虚拟列表的核心思想是只渲染可视区域的DOM
      • 传统方法,假设10000条评论,那就会渲染出10000个DOM,会很卡,浏览器真正慢的是 DOM,不是 JS。
      1
      comments.map(c => <Comment />)
      • 当DOM很多的时候,Layout计算慢,滚动也需要重排

      How

      • 如果用户屏幕大小就这么多,为什么要去渲染全部的评论,只需要用评论区容器的px / 每条评论容器的px 就是渲染出来的评论数。
      • 滚动时候用scrollTop,它表示滚动了多少px,1600px / 80px = 20条
      1
      startIndex = Math.floor(scrollTop / itemHeight)

      所以只需要渲染 startIndex + 评论数

  • 核心只有 4 步:
  1. 获取滚动距离 scrollTop
  2. 计算起始 index start = scrollTop / itemHeight
  3. 计算需要多少条 visibleCount = containerHeight / itemHeight
  4. 渲染 data.slice(start, start + visibleCount)

手写一个倒计时的拓展

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { useEffect, useState } from 'react'
import './App.css'

function App() {
const [count, setCount] = useState(100)
console.log(count)
useEffect(()=>{
//形成的闭包是什么???理论上一次为什么两次?
const timer = setInterval(()=>{
//setCount((prev)=>(prev > 0 ? prev - 1: 0 ))
setCount(count - 1)
},1000)

return ()=>clearInterval(timer)
},[])

return (
<div>{count}</div>
)
}

export default App

What

  • useEffect(fn,[]),[]表示依赖列表为空,意思是:

    effect 不依赖任何 state / props,因此只在组件首次挂载时执行一次。

  1. 所以第一次,如果使用函数式的更新,即使依赖项为[],也会拿到最新的prev
  2. 第二次由于没填写依赖项,永远拿的是第一个值,因为没有依赖,所以不会重新执行
  3. console.log两次是因为开发时有了strict模式,默认走两次,会让副作用出现。

React 的通信方式有哪些?

  • Props
  • Callback:父组件传函数,子组件调用
  • 兄弟组件通信:把状态提升到共同父组件
  • 全局状态管理

能把hook写在条件判断里吗?

  • 不能,从React的渲染机制来看,Props/State改变了就会重新渲染,如果我们写在了某一个循环判断里,然后成功渲染了,但是由于数据改变了,又会调用App()去渲染,这样就会多次渲染,会报错。

    Uncaught Error: Too many re-renders. React limits the number of renders to prevent an infinite loop.

图片的懒加载是怎么实现的?

  1. 原生:img标签里的loading属性设置为lazy
  2. Intersection Observer API
1
2
3
4
5
6
7
8
9
10
11
12
13
const images = document.querySelectorAll('img[data-src]');

const observer = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src; // 替换真实链接
observer.unobserve(img); // 停止观察该图片
}
});
});

images.forEach(img => observer.observe(img));

预热加载: 在 Intersection Observer 中设置 rootMargin: “200px”,让图片在进入视口前 200 像素就开始加载,用户几乎感知不到加载过程。

大模型这种“蹦字”的效果,前端是怎么拿到的?底层协议是什么?

  • 通过fetch 配合 ReadableStream,fetch配合ReadableStream
  • 使用 fetch 请求大模型接口时,返回的 response.body 是一个流
1
2
3
4
5
6
7
8
9
10
11
12
const response = await fetch('/api/chat', { method: 'POST', body: JSON.stringify({ prompt }) });
const reader = response.body.getReader(); // 获取读取器
const decoder = new TextDecoder(); // 负责将二进制字节流转为字符串,也可以转为UTF-8

while (true) {
const { done, value } = await reader.read(); // 读取下一块数据
if (done) break; // 读取完毕

const chunk = decoder.decode(value); // 此时拿到的是类似 "data: {"content": "你好"}" 的字符串
// 经过正则或 JSON 解析,提取出文字并渲染到页面上
processText(chunk);
}

响应式布局?

What

  • 页面根据设备尺寸自动调整布局(媒体查询、flex、grid)

Why

  • 适配手机、平板、PC提升用户体验

How

  • 使用 @media、flex/grid(使用弹性布局替代固定像素。)、百分比布局、rem/vw(相对单位)、移动端优先设计。

State和Props的区别

What

  • 都是js对象,用于保存信息并触发渲染,Props是组件的配置参数(外部传入),State是组件的私有状态(内部管理)
  • Props只读,State可变

如何区分一个变量是数组还是对象?

How

  1. typeof是没用的,只能认出基本类型,typeof {}和typeof []的结果都是object
  2. 用instanceof在原型链上找
1
2
3
4
const a = {}
const b = []
console.log(a instanceof Object) //true 如果是Array就是false
console.log(b instanceof Object) //true
  1. API,isArray()
  2. Object.prototype.toString.call()