面试记录
# vue
# 组件怎么封装?
- 当我们需要在多个页面或者多个项目中复用一段代码时,可以将这段代码封装为一个组件,以便于重复利用。
- 在需要使用该组件的父组件中,通过
import引入该组件。 - 在父组件中,使用
<my-component>标签即可使用该组件,并可以传入需要的属性。 - 父向子传递数据是通过
props,子向父是通过$emit触发事件; - 在组件内部不确定该位置是以何种形式的元素展示时,可以通过
slot占据这个位置,该位置的元素需要父组件以内容形式传递过来。
# 二维表格怎么实现(有排序功能)?
- 查看组件的文档,看看有没有对应的排序功能
- 和后端沟通,是否有接口是否有排序的功能
- 如果后端有排序的功能,当表头被点击时,触发排序事件,对表格数据进行排序,并重新渲染表格。
# 导航守卫有什么?
常用的导航守卫包括 beforeEach、beforeResolve、afterEach
beforeEach: 在进入路由之前执行,可以用来做登录校验等功能。beforeResolve: 在路由确认之前、异步路由组件解析完毕之后执行,可以用来确保异步组件的加载完成。afterEach: 在路由跳转完成后执行,可以用来做页面切换动画等功能。
# 导入Excel文件
- 创建
excel导入页面 - 点击
excel导入按钮,进入该页面 - 选中文件之后,使用JS库(如xlsx)读取Excel文件内容,并将其转换为JavaScript对象或数组
- 上传解析之后的数据
# 分页
# vuex怎么修改state的值
使用 mutations 修改状态: 在 Vuex 的 store 中,mutations 是用于修改状态的方法。你可以通过提交一个 mutation 来改变 state 中的值,这是一种同步的方式。
在某些情况下,你可能需要在异步操作完成后再修改状态,这时可以使用 actions。在 actions 中可以包含异步操作,然后再提交 mutations 来修改状态。
// 定义 action actions: { updateValueAsync({ commit }, payload) { setTimeout(() => { commit('updateValue', payload); }, 1000); } } // 在组件中分发 action this.$store.dispatch('updateValueAsync', newValue);1
2
3
4
5
6
7
8
9
10
11
# 事件总线怎么使用
Vue3如果想要实现这种机制,则需要通过mitt来进行实现
import mitt from 'mitt'
const emitter = mitt()
// 监听一个事件
emitter.on('foo', e => console.log('foo', e) )
// 监听所有事件
emitter.on('*', (type, e) => console.log(type, e) )
// 触发事件
emitter.emit('foo', { a: 'b' })
// 情况所有事件
emitter.all.clear()
// working with handler references:
function onFoo() {}
emitter.on('foo', onFoo) // 监听
emitter.off('foo', onFoo) // 卸载
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 主题颜色
- 主题配置:在前端项目中创建一个主题配置文件,用于存储不同主题的颜色变量或配置项。例如,可以创建一个
theme.js文件,里面包含不同主题的颜色变量,如主题的主色调、背景颜色、文本颜色等。 - 样式文件:在项目中创建一个样式文件,用于定义各个页面和组件的样式。这个样式文件会引用主题配置文件中的颜色变量。
- 主题切换功能:实现一个主题切换的功能,使用户能够在前端后台界面中切换不同的主题。可以使用 Vuex、React Context 或其他状态管理工具来管理当前主题的状态。
- 主题切换逻辑:在主题切换功能中,根据用户选择的主题,更新存储在状态管理工具中的当前主题。同时,更新主题配置文件中的颜色变量。
- 样式更新:通过监听当前主题的变化,实时更新应用程序中的样式。可以使用 CSS 变量、样式预处理器(如 Less、Sass)或 JavaScript 动态修改样式的方式来实现。确保页面和组件的样式与当前主题保持一致。
- 持久化主题设置:如果希望用户下次访问时保持之前选择的主题,可以使用浏览器的本地存储(localStorage 或 sessionStorage)将用户的主题设置持久化存储,并在应用初始化时读取这些设置。
# 断点
# 页面适配
通过flex和媒体查询结合使用
# amis
# 双向绑定
vue2双向绑定中,例如为对象添加一个属性,vue为什么检测不到属性修改
在 Vue 2.x 中,双向绑定是通过数据劫持和依赖追踪来实现的。Vue 使用了一个叫做 "响应式系统" 的机制来追踪数据的变化并更新视图。这个系统会在组件实例创建时对数据进行劫持,并在数据发生改变时通知相关的视图更新。
然而,Vue 在数据劫持时只会对初始化时已经存在的属性进行劫持,对于后来添加的属性,Vue 默认是无法检测到其修改的。这是因为 Vue 的响应式系统在初始化时会遍历对象的所有属性,并将其转换成 getter 和 setter 来实现双向绑定。但是对于后来添加的属性,Vue 并没有对其进行劫持。
如果你需要动态地给一个对象添加新的属性,并且希望 Vue 可以检测到这些属性的修改,可以使用 Vue.set 方法或 this.$set 方法来添加属性,这样 Vue 就会对新添加的属性进行劫持。
Object.defineProperty的原理是通过将数据属性转变为存取器属性的方式实现的属性读写代理。而Proxy则是因为这个内置的Proxy对象内部有一套监听机制,在传入handler对象作为参数构造代理对象后,一旦代理对象的某个操作触发,就会进入handler中对应注册的处理函数,此时我们就可以有选择的使用Reflect将操作转发被代理对象上。
# watchEffect和watch的区别
watchEffect 是Vue 3 中引入的一个新 API,只需要传入副作用函数,会自动追踪其内部使用的响应式数据,并在数据变化时自动重新执行副作用函数。适用于需要立即执行副作用函数的情况。
watch 需要指定要监听的具体响应式数据,并在回调函数中处理数据变化的逻辑。可以设置更多的选项来控制监听器的行为。适用于需要对数据变化进行更细粒度控制的情况。
# 适合在不同生命周期发送请求
created生命周期:在组件创建时,需要获取初始数据进行初始化。比如获取用户信息、获取配置信息等。
mounted生命周期:在组件挂载后,需要请求数据进行页面渲染。比如获取列表数据、获取广告数据等。
需要进行 DOM 操作的请求,比如获取某个元素的尺寸或位置信息,然后根据获取的信息动态设置样式或布局。
# React
# React 常用 Hooks 全整理 + 相似 Hook 对比
# 内置
| 分类 | Hook |
|---|---|
| 状态管理 | useState、useReducer |
| 副作用 | useEffect、useLayoutEffect、useInsertionEffect |
| 性能优化 | useMemo、useCallback |
| Ref 相关 | useRef、forwardRef、useImperativeHandle |
| Context | useContext |
| 并发特性 | useTransition、useDeferredValue |
| 唯一ID | useId |
| 外部数据订阅 | useSyncExternalStore |
| 调试 | useDebugValue |
| Hook 名称 | 核心作用 | 使用场景简述 |
|---|---|---|
| useState | 基础状态管理,定义组件响应式数据,修改触发重渲染 | 简单单个状态:开关、输入框值、计数 |
| useReducer | 复杂多联动状态,集中式 dispatch 修改状态 | 表单多字段、状态变更逻辑繁杂、多分支判断 |
| useEffect | 副作用钩子,DOM 绘制完成后异步执行副作用 | 接口请求、事件订阅、定时器、组件挂载 / 卸载逻辑 |
| useLayoutEffect | DOM 生成完毕、浏览器绘制前同步执行副作用 | 修改 DOM 布局、元素尺寸修正、依赖 DOM 布局的计算 |
| useMemo | 缓存计算返回值,依赖不变复用结果,避免重复计算 | 大列表筛选、复杂数学运算、昂贵派生数据 |
| useCallback | 缓存函数实例,依赖不变不重新创建函数 | 传递函数给子组件(配合 React.memo 优化渲染) |
| useRef | 存储可变数据 / DOM 实例,变更不触发组件重渲染 | 获取 DOM 节点、保存定时器、记录上一次数据 |
| forwardRef | 高阶方法,转发 ref,让父组件获取子组件 DOM / 实例 | 封装通用组件,父组件需要直接操作子组件 DOM |
| useContext | 读取全局上下文 Context 值,跨层级透传数据 | 主题配置、全局用户信息、多组件共享数据 |
| useTransition | 将更新标记为低优先级,非阻塞紧急渲染 | 大数据筛选、大列表渲染优化,防止输入卡顿 |
| useDeferredValue | 延迟派生状态更新,类似值防抖 | 搜索联想、根据大状态派生计算数据优化 |
| useId | 生成唯一稳定 ID(兼容 SSR) | 表单 label 绑定 id、避免服务端客户端 id 不一致 |
| useSyncExternalStore | 订阅第三方外部数据源 | 对接 Redux、原生 Storage、非 React 全局变量 |
# 封装工具类hooks
| Hook 名称 | 核心作用 | 使用场景简述 |
|---|---|---|
| useLocalStorage | 封装 localStorage 持久化状态,用法同 useState | 数据本地持久存储,刷新页面数据不丢失 |
| useBoolean | 封装布尔状态,内置 setTrue/setFalse/toggle | 弹窗显隐、开关控制、抽屉打开关闭 |
| useBackgroundBrightness | 获取背景色亮度,计算明暗 | 根据背景自动切换文字黑白颜色、主题适配 |
| useClientRect | 监听 DOM 尺寸位置,实时返回元素坐标宽高 | 弹窗定位、元素自适应、动态布局计算 |
| use-copy-to-clipboard | 剪贴板复制 Hook | 复制链接、复制验证码、复制编号 |
| use-countdown | 倒计时 Hook,传入秒数,返回剩余时间 + 开始 / 暂停 / 重置方法 | 信倒计时、活动倒计时、验证码 60s 冷却 |
| use-debounce | 防抖 Hook,对高频函数防抖(延迟执行),基于 useEffect + 定时器 | 搜索框输入联想、窗口 resize 频繁触发回调、滚动频繁回调 |
| use-double-click | 区分单击 / 双击事件 Hook | |
| use-event-listener | 统一封装 DOM 事件绑定,自动在组件挂载 addEventListener、卸载 removeEventListener(自动解绑,防止内存泄漏) | |
| use-responsive | 响应式断点 Hook,监听浏览器视口宽度,返回当前是移动端 / 平板 / PC 端 | |
| use-scroll-offset-top | 监听页面滚动,实时返回scrollTop滚动距离 | 导航栏滚动变色、吸顶、滚动进度条 |
| use-scroll-to-top | 一键回到顶部 Hook,返回scrollToTop()执行函数,调用页面平滑滚动至顶部 | |
| use-tabs | 标签页管理 Hook,维护标签激活项、切换方法、新增 / 删除标签 | 封装 tabs 组件逻辑,返回 activeKey、onChange、addTab、delTab |
# 封装业务类hooks
| Hook 名称 | 核心作用 | 使用场景简述 |
|---|---|---|
| useBluetoothConnection | 处理连接类 | 连接、重连、超时、断开 |
| useGameStatus | 比赛状态同步、状态转换 | |
| useRingCollector | 数据监听 | |
| usePermission | 处理权限 | 首次安装app需要获取权限 |
# 虚拟DOM
虚拟 DOM 是用 JS 对象描述真实 DOM 结构的树形数据,是真实 DOM 的抽象映射,本质就是普通 JavaScript 对象。 真实 DOM 操作开销大(创建、修改、重排重绘),虚拟 DOM 用来减少频繁操作真实 DOM。
createElement/h:生成 VNode(虚拟节点)
通过
createElement把 JSX / 模板编译成 VNode 树(整棵虚拟 DOM 树)Diff 算法:新旧 VNode 差异化对比
数据变化生成新 VNode 树,和旧 VNode 对比,只找出变化节点,而非全量刷新 DOM
Diff 的核心策略:
- 同层比较:只比较同一层级节点,不会跨层移动比较
- 类型不同直接替换,不会继续深度比较
- key 优化列表 Diff
把 Diff 结果更新到真实 DOM
# 列表频繁切换反复请求如何优化
Stale While Revalidate(先返回缓存,再后台刷新)
- Cache
缓存
- Deduplication
请求去重
- Revalidation
自动更新
- Cancellation
取消无效请求
常用工具React Query、SWR
# 图表频繁更新
减少 React Render 次数,减少图表重绘次数
- useRef缓存数据,然后定时刷新
- 批量更新
- React.memo
WebSocket
↓
pendingRef
↓
200ms批量更新
↓
dataRef
↓
echartsInstance.setOption()
----------------------------------
WebSocket
↓
ref缓存
↓
200~500ms批量刷新
↓
ECharts setOption
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Ant Design、Element Plus、MUI (Material UI) 选型对比 & 选择方案
| 维度 | Ant Design(React) | Element(Plus)(Vue) | MUI(React) |
|---|---|---|---|
| 设计风格 | 中式商务稳重 | 传统后台简约 | 谷歌 MD 现代极简 |
| 组件丰富度 | ★★★★★(B 端顶配) | ★★★★ | ★★★(基础优秀,B 端缺高级组件) |
| 上手难度 | 中等 | 极低 | 偏高(样式自定义成本高) |
| 适配场景 | B 端后台首选 | Vue 小后台快速开发 | C 端、国际化产品 |
| 生态 | Pro 组件全家桶 | 配套生态少 | 大量第三方 MD 扩展组件 |
# Taro 和 uni-app 有什么区别?
| 对比项 | Taro | uni-app |
|---|---|---|
| 开发语言 | React / Vue | Vue |
| 语法风格 | React Hooks | Vue Composition API |
| 学习成本 | React开发者低 | Vue开发者低 |
| 小程序支持 | 强 | 强 |
| H5支持 | 强 | 强 |
| App支持 | 强 | 非常强 |
| 插件生态 | React生态 | uni生态 |
| 社区 | 较大 | 国内非常大 |
| 企业使用 | 京东系较多 | 外包、中小企业较多 |
# html
- Doctype 是用于让标准通用语言解析器知道怎么解析
- <img style="display:none;”src="http://m.xxxx.cn/test.png”/>会加载test.png吗
# js
# Promise三种状态
pending :等待状态,比如正在进行网络请求,或者定时器没有到时间的时候。
fulfilled: 满足状态,当我们主动回调了resolve()时,接口调用成功,就处于该状态,并且会回调.then()
rejcet:拒绝状态,当我们主动回调了reject()时,接口调用失败,就处于该状态,并且会回调.catch()
# Promise怎么取最快的一个
Promise.any()
返回一个 Promise,如果数组中的任何一个 Promise 成功,则该 Promise 解析(resolve)。如果数组中所有 Promise 都失败,则该 Promise 拒绝(reject)。
Promise.race()
返回一个 Promise,如果数组中的任何一个 Promise 完成(无论成功或失败),则该 Promise 立即解析(resolve)或拒绝(reject)。返回的结果则是所有 promise 中最先返回的结果,不关心是成功还是失败。
# Promise.all/any/race的区别
Promise.all 等待所有 Promise 解析,并返回所有结果的数组。
Promise.any 等待至少一个 Promise 解析,并返回第一个解析的结果。
Promise.race 等待第一个 Promise 解析,并返回第一个解析的结果或失败的原因。
# 列出你熟悉的es6特性
- 箭头函数(Arrow Functions):使用简洁的语法定义函数,并绑定了词法作用域。箭头函数没有自己的
this值,而是继承外部作用域的this值。 - const 和 let:引入块级作用域的变量声明关键字。
const声明一个常量,而let声明一个可变的变量。 - 模板字面量(Template Literals):使用反引号(
)包裹的字符串,可以在其中插入变量或表达式,使用${}` 进行插值。 - 解构赋值(Destructuring Assignment):通过模式匹配的方式从数组或对象中提取值,并赋值给对应的变量。
- 默认参数(Default Parameters):在函数参数列表中为参数设置默认值,简化了函数的使用,并允许在调用函数时省略某些参数。
- 扩展运算符(Spread Operator):用三个点(...)表示,可以在函数调用、数组字面量、对象字面量等处展开数组或对象。
- 类和模块(Classes and Modules):引入了类和模块的概念,使得面向对象编程更加直观和易用。
- 箭头函数(Arrow Functions):使用简洁的语法定义函数,并绑定了词法作用域。箭头函数没有自己的
this值,而是继承外部作用域的this值。 - Promise:用于处理异步操作的对象,提供了一种更优雅的方式来处理回调函数地狱。
- 迭代器和生成器(Iterators and Generators):迭代器提供了一种遍历数据的通用接口,生成器是一种特殊的函数,可以通过暂停和恢复的方式产生多个值。
- 模块化(Module System):引入了模块化的概念,通过
export和import关键字实现模块的导出和导入。
# JavaScript 有哪几种数据类型?
基本数据类型:
- 字符串(String):表示文本数据,用于存储和操作文本信息。例如:
"Hello World"。 - 数字(Number):表示数值数据,用于存储和操作数值信息。包括整数、浮点数、负数等。例如:
42、3.14。 - 布尔值(Boolean):表示逻辑值,只有两个取值:
true(真)和false(假)。用于控制程序的流程和逻辑判断。 - undefined:表示未定义的值,当声明了一个变量但未给它赋值时,该变量的值为
undefined。 - null:表示空值或空对象引用。用于显式地表示一个变量没有值。
特殊的数据类型:
- 对象(Object):表示复杂数据结构,可以包含多个键值对。对象是属性和方法的集合,用于组织和存储相关的数据和功能。
- 数组(Array):表示一组有序的数据集合。数组可以包含任意类型的数据,并通过索引来访问和操作数组中的元素。
- 函数(Function):是一段可执行的代码块,用于封装和执行特定的任务。函数可以接收参数、执行特定的操作,并返回结果。
- Symbol:表示唯一的标识符,用于创建对象的私有属性或键。
# css3中transform的作用?
- 平移(Translate):使用
translate函数可以将元素在水平和垂直方向上移动指定的距离。例如:transform: translate(100px, 50px);将元素向右平移 100 像素,向下平移 50 像素。 - 缩放(Scale):使用
scale函数可以按比例缩放元素的大小。例如:transform: scale(1.5);将元素放大到原来的 1.5 倍。 - 旋转(Rotate):使用
rotate函数可以使元素按指定角度进行旋转。例如:transform: rotate(45deg);将元素顺时针旋转 45 度。 - 倾斜(Skew):使用
skew函数可以使元素按指定角度进行倾斜。例如:transform: skew(10deg, 20deg);将元素水平倾斜 10 度,垂直倾斜 20 度。 - 变形原点(Transform Origin):使用
transform-origin属性可以设置元素变换的基准点,默认是元素的中心点。 - 组合变换(Transforms Combination):可以通过将多个变换组合在一起,实现复杂的动画和效果。例如:
transform: translate(100px, 50px) rotate(45deg) scale(1.5);将元素先平移,再旋转,最后缩放。
transform 属性还可以与过渡(transition)、动画(animation)等属性结合使用,实现更丰富的动态效果。
# Js中0.1+0.2为什么不等于0.3? 怎么解决?
在 JavaScript 中,0.1 + 0.2 不等于 0.3 的原因是浮点数的精度问题。由于计算机内部使用二进制来表示浮点数,而二进制表示有时无法精确地表示某些十进制小数。因此,在进行浮点数计算时,可能会出现舍入误差,导致结果不完全精确。
使用整数进行计算:将需要计算的小数转换为整数,进行整数运算,最后再将结果转换回小数。例如,将 0.1 和 0.2 分别乘以 10,得到 1 和 2,然后进行整数相加,得到 3,最后再除以 10,得到 0.3。
const result = (0.1 * 10 + 0.2 * 10) / 10; // 结果为 0.31使用精确计算库:可以使用一些第三方库,如 Decimal.js、Big.js 等,来进行精确的十进制计算。这些库提供了高精度的十进制数值计算,可以避免浮点数精度问题。
// 使用 Decimal.js 示例 const Decimal = require('decimal.js'); const result = new Decimal(0.1).plus(0.2).toNumber(); // 结果为 0.31
2
3
4一列数的规则如下:1、1、2、3、5、8、13、21、34......求第30 位数是多少,用递归方式解答。
斐波那契数列(Fibonacci Sequence)的规则是,从第三个数开始,每个数都是前两个数的和。
function fibonacci(n) { if (n === 1 || n === 2) { return 1; } else { return fibonacci(n - 1) + fibonacci(n - 2); } } const result = fibonacci(30); console.log(result); // 输出第 30 位数的值1
2
3
4
5
6
7
8
9
10在上面的代码中,
fibonacci函数接受一个参数n,表示要求解的斐波那契数列的位置。当n为 1 或 2 时,直接返回 1,作为递归的基准情况。否则,通过递归调用fibonacci函数来计算前两个数的和,并返回结果。运行上述代码,将得到斐波那契数列中第 30 位的值,即 832040。
编写个函数来获取url 的参数值(请用正则方式解答)
var urI ="http://www.xxx. com/index.html?key0-0&key1=1&key2=2";function getUrlParameter(url, parameter) { // 创建正则表达式,匹配 parameter=xxx 这样的字符串 const regex = new RegExp(`[?&]${parameter}=([^&#]*)`); // 在 URL 中查找匹配的参数值 const match = url.match(regex); // 如果有匹配的结果,则返回参数值,否则返回 null return match ? match[1] : null; } // 示例用法 const url = 'https://www.example.com/?name=John&age=25'; const parameterValue = getUrlParameter(url, 'age'); console.log(parameterValue); // 输出 251
2
3
4
5
6
7
8
9
10
11
12
13
14
15在上述代码中,
getUrlParameter函数接受两个参数:url是完整的 URL 字符串,parameter是要获取的参数名。函数内部使用正则表达式创建一个模式,通过匹配
parameter=xxx这样的字符串来提取参数值。然后,在给定的 URL 中使用match方法进行匹配,并获取匹配结果数组。如果有匹配的结果,则返回第一个捕获组的值,即参数值。如果没有匹配结果,则返回 null。请问下面打印出来的结果是?
for (var i=0;i<3;i++) { setTimeout(function () { console.log("a:"+i); },0); console.log("b:"+i); } b:0 b:1 b:2 a:3 a:3 a:31
2
3
4
5
6
7
8
9
10
11
12
13
# 列举几种常见设计模式并简述其使用场景
单例模式(Singleton Pattern):用于确保类只有一个实例,并提供一个全局访问点。适用于需要全局共享资源、管理唯一对象或限制对象创建的场景。
工厂模式(Factory Pattern):通过工厂方法创建对象,而不是直接通过构造函数实例化对象。适用于需要根据不同条件创建不同对象的场景,提供了灵活的对象创建机制。
观察者模式(Observer Pattern):定义了对象间的一对多依赖关系,当一个对象状态改变时,其所有依赖者都会收到通知并自动更新。适用于对象间存在一对多的关系,当一个对象改变时,通知其他对象进行相应的处理。
策略模式(Strategy Pattern):定义一系列算法,将它们封装起来,并使它们可以相互替换。适用于需要在运行时根据不同情况选择不同算法的场景,提供了灵活的算法选择机制。
适配器模式(Adapter Pattern):将一个类的接口转换成客户端所期望的另一个接口,使原本不兼容的类可以合作。适用于需要将一个类的接口适配到另一个类的场景,提供了不同接口之间的桥梁。
装饰器模式(Decorator Pattern):动态地给一个对象添加额外的职责,而不影响其他对象。适用于需要动态添加功能、职责或行为的场景,提供了灵活的功能扩展机制。
代理模式(Proxy Pattern):为其他对象提供一种代理以控制对这个对象的访问。适用于需要控制对对象的访问、提供额外功能或隐藏对象的场景。
Vue.js 使用了以下几种设计模式:
- MVVM(Model-View-ViewModel)模式:Vue.js 基于 MVVM 模式进行开发。在 MVVM 模式中,视图(View)与数据模型(Model)之间通过一个称为 ViewModel 的中间层进行连接和通信。Vue.js 的响应式数据绑定机制和模板语法使得开发者可以直接操作数据,而不需要手动更新 DOM,从而实现了视图和数据的自动同步。
- 组件模式(Component Pattern):Vue.js 的核心思想是组件化,将应用程序拆分为多个可复用的组件。组件模式通过将 UI、数据和逻辑封装在一个组件中,提供了更高的可维护性和可复用性。
- 观察者模式(Observer Pattern):Vue.js 使用观察者模式实现了响应式数据绑定。当数据发生变化时,Vue.js 会自动通知相关的视图进行更新。这种机制使得开发者无需手动追踪数据变化,提高了开发效率。
- 依赖注入模式(Dependency Injection Pattern):Vue.js 使用依赖注入模式实现了组件之间的依赖关系管理。通过在组件定义中声明依赖关系,Vue.js 可以在组件实例化时自动注入依赖,简化了组件间的通信和协作。
- 虚拟 DOM 模式(Virtual DOM Pattern):Vue.js 使用虚拟 DOM 模式优化了 DOM 操作的性能。Vue.js 在内部维护了一个虚拟 DOM 树,通过比较新旧虚拟 DOM 树的差异,最小化了对实际 DOM 的操作,从而提高了渲染性能。
# 什么是闭包?
闭包是指一个函数可以访问并操作其外部函数作用域中的变量和参数,即使外部函数已经执行完毕,这些变量依然可以被访问。
function f1(){
var n=999;
function f2(){
console.log(n);
}
return f2;
}
var result=f1();
result(); // 999
2
3
4
5
6
7
8
9
如何理解作用域,es5中和es6有什么区别?
作用域 (Scope) 是指程序中定义变量的区域,定义在该区域内的变量只能在该区域内访问。在JavaScript中,作用域分为全局作用域和函数作用域两种。
在ES5中,作用域主要通过函数来实现。每个函数都有自己的作用域,函数内定义的变量只能在该函数内部访问,函数外部无法访问。如果在函数内部使用var关键字定义变量,该变量的作用域为函数作用域。如果定义在函数外部的变量,则其作用域为全局作用域,可以在整个程序中被访问。
在ES6中,引入了块级作用域 (Block Scope) ,通过let和const关键字来定义变量。使用let和const定义的变量的作用域为块级作用域,即在花括号0内定义的变量只能在花括号内访问,花括号外部无法访问。在块级作用域中,变量会形成一个封闭的作用域,不会被外部的变量所干扰。而且,ES6中的const关键字定义的变量是常量,不能被重新赋值,保证了代码的安全性。
总的来说,ES6中引入了块级作用域,让变量的作用域更加精细化,增强了代码的可读性和可维护性。
写出三种用js实现的类方法
基于原型的实现方式
// 手机(父级) function Phone(brand,price){ this.brand = brand this.price = price } Phone.prototype.call = function(){ console.log("我可u打电话!") } // 智能手机(子级) function SmartPhone(brand,price,color,size){ Phone.call(this,brand,price) // 调用父类的构造函数 this.color = color this.size = size } // 设置子级构造函数的原型(我的实例对象原型有父级的方法) SmartPhone.prototype = new Phone // 声明子类的方法 SmartPhone.prototypr.photo = function(){ console.log("我可以拍照!") } SmartPhone.prototypr.playGame = function(){ console.log("我可以玩游戏!") } const chuizi = new SmartPhone('锤子',2499,'黑色','5.5inch')1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27基于类的实现方式
// 父类 class Phone{ // 构造方法 名字不能修改 constructor(brand,price){ this.brand = brand this.price = price } // 方法必须使用该语法,不能使用ES5的对象完整形式 call(){ console.log("我可u打电话!") } } class SmartPhone extends Phone{ // 构造方法 constructor(brand,price,color,size){ super(brand,price) // 调用父类的构造函数 this.color = color this.size = size } photo(){ console.log("拍照") } playGame(){ console.log("玩游戏") } } const xiaomi = new SmartPhone('小米',799,'黑色','4.7inch')1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29工厂函数的实现方式
写出一个原型链继承的例子
原型链继承是JavaScript中常见的一种继承方式,它基于原型链的特性来实现继承。通过将子类的原型设置为父类的实例,来实现子类继承父类的属性和方法。
function Person(name) { this.name = name this.printNama = function () { console.log('Person printNama'); } } Person.prototype.commen = function () { console.log('commenMethods') } function Student(name, score) { Person.call(this, name) this.score = score this.printScore = function () { console.log('Student printScore'); } } Student.prototype = new Person() let person = new Person('小红', 30) let student = new Student('小敏', 20) console.log(Student.commen === Person.commen) console.log('student', student) console.log('person', person)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22数组去重
使用 Set
const arr = [1, 2, 2, 3, 3, 4, 5, 5]; const uniqueArr = [...new Set(arr)];1
2使用 indexOf 或 includes
可以遍历原始数组,将元素逐个添加到一个新数组中,但在添加之前先判断新数组中是否已经包含该元素。
const arr = [1, 2, 2, 3, 3, 4, 5, 5]; const uniqueArr = []; arr.forEach(item => { if (!uniqueArr.includes(item)) { uniqueArr.push(item); } });1
2
3
4
5
6
7使用 reduce
对数组进行遍历,如果结果数组中已经包含当前元素,则不添加,否则添加到结果数组中。
const arr = [1, 2, 2, 3, 3, 4, 5, 5]; const uniqueArr = arr.reduce((acc, curr) => { if (!acc.includes(curr)) { acc.push(curr); } return acc; }, []);1
2
3
4
5
6
7使用 filter
对数组进行筛选,只保留第一次出现的元素。
const arr = [1, 2, 2, 3, 3, 4, 5, 5]; const uniqueArr = arr.filter((item, index, self) => { return self.indexOf(item) === index; });1
2
3
4获取对象的 key 值
Object.keys() 方法
const obj = { a: 1, b: 2, c: 3 }; const keys = Object.keys(obj); console.log(keys); // ['a', 'b', 'c']1
2
3for...in 循环
const obj = { a: 1, b: 2, c: 3 }; for (let key in obj) { console.log(key); // 'a', 'b', 'c' }1
2
3
4Reflect.ownKeys() 方法
const obj = { a: 1, b: 2, c: 3 }; const keys = Reflect.ownKeys(obj); console.log(keys); // ['a', 'b', 'c']1
2
3
实现页面中的图表数据实时更新
- 设置定时器或使用 WebSocket:在页面加载或数据更新后,使用定时器/轮询(
setInterval)或 WebSocket 来定期发送请求获取最新的数据。 - 更新图表数据:在定时器的回调函数中或 WebSocket 接收到新数据时,更新图表的数据。
- 刷新图表:调用 ECharts 提供的刷新图表方法(
echartsInstance.setOption())来更新图表的显示
- 设置定时器或使用 WebSocket:在页面加载或数据更新后,使用定时器/轮询(
for循环遍历出0 1 2
使用立即执行函数
我们使用了立即执行函数(IIFE)来创建闭包,并将
i的值作为参数传递给闭包内的函数。这样每次循环都会创建一个新的闭包,并且在setTimeout的回调函数中使用闭包内保存的num值,保证了打印的结果为 0、1、2。function test() { for (var i = 0; i < 3; i++) { // 使用立即执行函数,创建一个闭包并传入 i 的值 (function (num) { setTimeout(() => { console.log(num); }, 0); })(i); } } test();1
2
3
4
5
6
7
8
9
10
11
12修改关键字
由于
let创建的变量具有块级作用域,每次循环都会创建一个新的变量i,因此在setTimeout的回调函数中使用的i值是每次循环时的当前值。function test() { for (let i = 0; i < 3; i++) { setTimeout(() => { console.log(i); }, 0); } } test();1
2
3
4
5
6
7
8
9
箭头函数与普通函数的区别
- 箭头函数不会创建自己的this,它只会从自己的作用域链的上一层继承
this。 - 箭头函数继承而来的this指向永远不变
- .call()/.apply()/.bind()无法改变箭头函数中this的指向
- 箭头函数不能作为构造函数使用
- 箭头函数没有自己的arguments
- 箭头函数没有原型prototype
- 箭头函数不会创建自己的this,它只会从自己的作用域链的上一层继承
在 JavaScript 中,实现继承的方式
- 原型链继承是 JavaScript 中最基本的继承方式。**通过将父类的实例作为子类的原型,子类可以继承父类的属性和方法。**但是这种方式有一个缺点,就是所有子类实例共享同一个父类实例,可能会导致属性共享和修改的问题。
- 构造函数继承是通过在子类的构造函数中调用父类的构造函数来实现继承。通过这种方式,子类可以拥有独立的属性,避免了原型链继承的属性共享问题。
- 原型式继承是基于现有对象创建新对象的一种继承方式。它通过 Object.create() 方法创建一个新对象,并指定现有对象作为新对象的原型。
- ES6 中实现继承的使用
class和extends实现继承,解决了原型链继承和构造函数继承的问题,子类可以继承父类的属性和方法,而且子类实例也不会共享同一个父类实例。class语法只是一种语法糖,本质上还是基于原型链的继承方式。
# ts
type和interface有什么区别
相同点
- 用来描述对象或函数的类型,只是语法不同
- interface 用 extends 来实现扩展,type 使用 & 实现扩展
不同点
- type不仅可以为对象指定类型,实际上可以为任意类型指定别名/联合类型/元组
- interface能够合并声明
- ts怎么取函数的返回类型
在 TypeScript 中,要获取函数返回值的类型,可以使用
ReturnType<T>工具类型。这个工具类型可以从函数类型中提取返回值类型。function add(a: number, b: number): number { return a + b; } type AddReturnType = ReturnType<typeof add>; // AddReturnType 的类型将被推断为 number1
2
3
4
5
6在 TypeScript 中,常见的属性
number:表示数字类型。
string:表示字符串类型。
boolean:表示布尔类型,值为
true或false。Array:表示数组类型,可以使用
Type[]或者Array<Type>两种形式。object:表示对象类型。
any:表示任意类型,可以赋予任何类型的值。
void:表示没有返回值的函数类型。
null 和 undefined:表示 null 和 undefined 类型。
Tuple:表示元组类型,可以包含多种类型的值。
enum:表示枚举类型,用于定义一组命名的常数。
union:表示联合类型,可以指定多种类型中的一种。
interface:表示接口类型,用于定义对象的结构。
# uniapp
- uniapp 路由跳转方式有什么?
- 通过
uni.navigateTo方法进行跳转,该方法会将目标页面压入栈中,可以通过返回键返回上一个页面。 - 通过
uni.redirectTo方法进行跳转,该方法会关闭当前页面,打开目标页面。 - 通过
uni.reLaunch方法进行跳转,该方法会关闭所有页面,打开目标页面。 - 通过
uni.switchTab方法进行跳转,该方法只能用于跳转到 tabBar 页面,并且会关闭所有非 tabBar 页面。 - 通过
uni.navigateBack方法进行跳转,该方法用于返回上一个页面。
- 通过
- request异步改同步
- 如果非要将
uni.request异步请求改为同步请求,可以考虑使用async/await或者Promise来实现。 - 使用时可以直接调用
requestSync函数,并使用await关键字等待请求结果的返回:
- 如果非要将
- 组件生命周期
- beforeCreate在实例初始化之前被调用。
- created在实例创建完成后被立即调用。
- beforeMount在挂载开始之前被调用。
- mounted挂载到实例上去之后调用。
- beforeUpdate数据更新时调用,发生在虚拟 DOM 打补丁之前。
- updated由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。
- beforeDestroy实例销毁之前调用。在这一步,实例仍然完全可用。
- destroyedVue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。
- 页面生命周期
- onInit
- onLoad
- onShow
- onReady
- onHide
- onUnload
- onResize
- onPullDownRefresh
- onReachBottom
- onTabItemTap
- onShareAppMessage
- onPageScroll
- onNavigationBarButtonTap
- onBackPress
- onNavigationBarSearchInputChanged
- onNavigationBarSearchInputConfirmed
- onNavigationBarSearchInputClicked
- onShareTimeline
- onAddToFavorites
# CSS
在 CSS 中实现居中可以有多种方式?
- 使用
text-align: center居中内联元素。 - 使用
margin: auto居中块级元素,前提是该元素有固定宽度。 - 使用
position: absolute和transform: translate居中任意元素。通过设置top: 50%和left: 50%将元素的左上角移动到父元素的中心,然后使用transform: translate(-50%, -50%)将元素的中心与父元素的中心对齐。 - 使用 Flexbox 布局,在父容器上设置
display: flex,并设置justify-content和align-items属性实现居中。 - 使用 Grid 布局,在父容器上设置
display: grid,并设置place-items: center属性实现居中。
- 使用
不改变child1和child2的z-index,请让child2覆盖在child1上面
为box1添加
transform: translateZ(1px);<template> <div class="box1"> <div class="child1">child1</div> </div> <div class="box2"> <div class="child2">child2</div> </div> </template> <style scoped> .box1,.box2{ position: relative; z-index: auto; } .child1{ width: 200px; height: 100px; background: #168bf5; position: absolute; top: 0; left: 0; z-index: 2; } .child2{ width: 100px; height: 200px; background: #32c292; position: absolute; top: 0; left: 0; z-index: 1; } </style>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33请描述一下盒子模式,并如何实现对盒子模式的切换
所有的网页元素都看成一个盒子,它具有: content,padding,border,margin (opens new window) 四个属性,这就是盒子模型。
# 盒子模型有几种模式?
1、W3C标准盒子模型 2、IE盒子模型
标准模式下,一个块的宽度 = width+padding(内边距)+border(边框)+margin(外边距) 怪异模式下,一个块的宽度 = width+margin(外边距) (即怪异模式下,width包含了border以及padding)
box-sizing:content-box; 将采用标准模式的盒子模型标准
box-sizing:border-box; 将采用怪异模式的盒子模型标准
请描述css对页面元素进行隐藏的方式和特性
display属性
display: none;visibility属性
visibility: hidden;opacity属性
opacity: @;position和z-index属性
clip属性
position: absolute;clip: rect(@,9,@,@);父元素高度塌陷是什么,如何解决?
清除浮动
.parent{ overflow: hidden; } /*或*/ .parent{ display: table; clear: both; }1
2
3
4
5
6
7
8
9使用伪元素清除浮动
.parent::after{ content:''; display: block; clear: both; }1
2
3
4
5使用BFC
.parent{ overflow: auto; display: flow-root;/*或使用display: block;*/ }1
2
3
4
如何用 CSS 实现一个三角形
利用盒模型的
border属性上下左右边框交界处会呈现出平滑的斜线这个特点,通过设置不同的上下左右边框宽度或者颜色即可得到三角形或者梯形。div{ width: 0; height: 0; border: 10px solid red; border-top-color: transparent; border-left-color: transparent; border-right-color: transparent; }1
2
3
4
5
6
7
8
# 计算机网络
TCP/IP TCP/IP(Transmission Control Protocol/Internet Protocol,传输控制协议/网际协议)是互联网相关的基础协议,由TCP和IP两部分组成。其中,TCP负责提供可靠的数据传输,而IP负责在网络中寻址和路由。
TCP/IP协议簇是互联网的基础,它定义了计算机之间的通信规则,包括了应用层、传输层、网络层和数据链路层四个层次。
应用层主要定义了应用程序之间的通信规则,如HTTP、FTP、SMTP等。
传输层主要负责数据的传输,保证数据的可靠性和完整性,常用的协议有TCP和UDP。
网络层主要负责网络寻址和路由,如IP协议。
数据链路层主要负责物理层面上的数据传输,如以太网协议。
通过TCP/IP协议簇,不同的计算机和设备可以在互联网上进行数据通信,实现了信息的全球性传输。
http 请求返回状态码 204 表示请求是成功的处理,但没有返回实体内容
https和http的区别
- 安全性:
- HTTP:是一种不安全的协议,数据传输是明文的,容易被攻击者窃听和篡改。
- HTTPS:是一种安全的协议,通过使用 SSL/TLS 加密技术,对数据进行加密和认证,保证数据传输的安全性和完整性。
- 加密:
- HTTP:不对数据进行加密,数据以明文形式传输。
- HTTPS:使用 SSL/TLS 加密技术,将数据加密后再进行传输,使得数据无法被窃听和破解。
- 默认端口:
- HTTP:默认端口是 80。
- HTTPS:默认端口是 443。
- 证书和身份验证:
- HTTP:不涉及证书和身份验证。
- HTTPS:需要使用 SSL/TLS 证书来对服务器进行身份验证,并确保数据传输的安全性。
- SEO 影响:
- HTTP:对搜索引擎优化(SEO)更友好,搜索引擎更容易抓取和索引 HTTP 网页。
- HTTPS:由于安全性的考虑,搜索引擎越来越倾向于将 HTTPS 网页作为排名因素,并给予更高的优先级。
总结来说,HTTPS 提供了更高的安全性和数据保护,适用于需要保护敏感信息的网站和应用程序,如电子商务网站、银行网站等。而 HTTP 适用于对安全性要求不高的网站或无需保护敏感信息的场景。
在实际开发中,对于需要传输敏感数据或保护用户隐私的网站和应用,强烈建议使用 HTTPS 来确保数据的安全和完整性。
- 安全性:
当你在浏览器的地址栏中输入一个 URL会发生什么
- URL 解析: 浏览器会解析输入的 URL,将其分解为以下几个部分:协议(例如 HTTP、HTTPS)、主机名、端口号(如果有)、路径(指定请求的资源路径)、查询参数附加在(URL 后面的键值对参数)和片段标识符(指定文档内的特定位置)。
- DNS 查询: 浏览器将解析后的主机名发送给 DNS(域名系统)服务器,以获取主机的 IP 地址。DNS 解析将域名转换为相应的 IP 地址,以便浏览器能够与主机建立连接。
- 建立连接: 浏览器使用解析后的 IP 地址和端口号(如果指定了端口号)与服务器建立网络连接。通常使用 TCP(传输控制协议)作为传输协议,建立可靠的连接。
- 发送请求: 浏览器向服务器发送一个 HTTP 请求,其中包含请求方法(GET、POST 等)、请求头(包含浏览器和请求的相关信息)和请求体(对于 POST 请求,可能包含数据)。
- 服务器处理请求: 服务器接收到浏览器发送的请求后,根据请求的路径和其他参数来处理请求。服务器可能会执行相应的处理逻辑,读取数据库、生成动态内容等,并返回相应的响应。
- 接收响应: 浏览器接收到服务器发送的响应,响应包括状态码、响应头和响应体。常见的状态码有 200 表示成功、404 表示资源未找到等。
- 渲染页面: 浏览器根据接收到的响应内容进行页面的渲染和呈现,解析 HTML、CSS 和 JavaScript,并将其显示在浏览器窗口中。浏览器会按照指定的渲染流程处理页面元素、布局和样式,最终将页面呈现给用户。
- 关闭连接: 当页面渲染完成后,浏览器和服务器之间的连接会被关闭,释放网络资源。
# 构建工具
Webpack 打包速度提升
- 减少打包文件的体积:通过使用代码分割(Code Splitting)和懒加载(Lazy Loading)等技术,将代码拆分成多个小模块,按需加载,减少初始加载的文件体积,提高页面的响应速度。
- 配置合理的模块解析规则:在 Webpack 的配置中,可以通过配置 resolve.extensions 和 resolve.alias 来优化模块解析速度。将常用的文件类型设置为 extensions,配置别名可以加快模块查找的速度。
- 使用缓存和持久化缓存:Webpack 提供了缓存机制,可以通过配置 cache 和 cacheLoader 来启用缓存,减少重复的构建过程。另外,可以使用持久化缓存,例如使用 babel-loader 的缓存选项,将转译后的文件缓存起来,避免每次重新转译。
- 使用多进程/多实例构建:通过配置 parallel-webpack 或者 HappyPack 插件等,可以启用多进程/多实例构建,利用多核 CPU 并行处理任务,提高构建速度。
- 优化 loader 和插件配置:合理配置 loader 和插件选项,避免不必要的处理和转换。例如,针对特定的文件类型,使用更轻量的 loader 或者排除一些不必要的目录。
- 使用 Tree Shaking:通过配置 Webpack 的 optimization.usedExports 选项,可以启用 Tree Shaking,去除未使用的代码,减少最终打包文件的体积。
- 使用压缩工具:在生产环境下,使用压缩工具(例如 UglifyJS、Terser)对打包后的代码进行压缩和混淆,减小文件体积。
- 开启缓存组:对于使用了大量第三方库的项目,可以通过配置 optimization.splitChunks.cacheGroups 来开启缓存组,将公共模块抽离出来,避免重复打包。
Webpack图片怎么打包
在 Webpack 中,处理图片资源的打包可以通过
file-loader或url-loader来实现。这两个 loader 可以将图片文件转换成打包后的文件,并返回文件的 URL,使得图片能够被正确引用和加载。- 安装
url-loader和file-loader:
npm install url-loader file-loader --save-dev1在 Webpack 配置中添加 loader:
module.exports = { // ...其他配置 module: { rules: [ { test: /\.(png|jpe?g|gif|svg)$/i, use: [ { loader: 'url-loader', options: { limit: 8192, // 图片大小小于 8KB 时,将会转成 base64 编码,否则使用 file-loader name: 'images/[name].[hash:8].[ext]', // 输出的文件路径和名称规则 }, }, ], }, ], }, };1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20在上面的配置中,我们使用
url-loader来处理图片资源。当图片大小小于 8KB (limit: 8192) 时,会将图片转换成 base64 编码,以减少 HTTP 请求,提高加载速度。对于大于 8KB 的图片,url-loader会使用file-loader来处理,将图片复制到输出目录中,并返回相应的文件路径。你可以根据自己的需求,调整
limit参数和name参数来控制打包后图片的处理方式和输出路径。
- 安装
# 小程序
正式发布后,新旧版本有冲突,缓存何时会被清除?
在旧版代码里配置发现更新重启小程序更新的话,新版本上线24小时内用户第二次打开小程序(冷启动)时会更成新版本。24小时以后打开时会先更新最新版本再打开。使用这个方法进行UpdateManager | 微信开放文档 (qq.com) (opens new window)处理
const updateManager = wx.getUpdateManager() updateManager.onCheckForUpdate(function (res) { // 请求完新版本信息的回调 console.log(res.hasUpdate) }) updateManager.onUpdateReady(function () { wx.showModal({ title: '更新提示', content: '新版本已经准备好,是否重启应用?', success: function (res) { if (res.confirm) { // 新的版本已经下载好,调用 applyUpdate 应用新版本并重启 updateManager.applyUpdate() } } }) }) updateManager.onUpdateFailed(function () { // 新版本下载失败 })1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23小程序性能优化
- 减少 HTTP 请求:合并和压缩资源文件,减少网络请求次数,提高加载速度。
- 图片优化:使用适当的图片格式,压缩图片大小,使用懒加载技术加载图片,减少首屏加载时间。
- 页面渲染优化:避免使用过多的 DOM 节点和样式,减少页面复杂度,提高渲染速度。
- 脚本优化:避免使用过多的全局变量和闭包,优化 JavaScript 代码,减少不必要的计算和重复操作。
- 数据缓存:合理使用小程序的缓存机制,将重要数据缓存到本地,减少网络请求。
- 使用合适的组件和 API:选择适合的小程序组件和 API,避免使用过多的无效功能,提高性能。
- 避免频繁的数据更新和渲染:减少页面中频繁的数据更新和渲染操作,尽量使用批量更新的方式进行操作。
- 分包加载:对于较大的小程序,可以将页面和资源进行分包加载,按需加载,减少首次加载时间。
- 使用 Web Worker:对于一些复杂的计算任务,可以考虑使用 Web Worker 进行异步处理,避免阻塞主线程。
- 定期检查性能:使用性能分析工具进行定期检查和优化,查找性能瓶颈并针对性地进行优化。
# 其他
- 学习前端新技术怎么学
- 先去看看官方文档
- 如果同一个技术有多种实现的时候
- 去github看看star
- 看看stack overflow对这些实现的评价
- 实践是学习新技术的关键。尝试在自己的项目中应用所学的知识
- 加入相关的技术社区,如论坛、社交媒体群组、开发者社区等。与其他开发者交流和分享经验,参与讨论,解答问题。
- 优缺点
- 具备持续学习的意识
- 与团队成员合作,具备良好的沟通和协作能力
- 目前处于初级阶段,可能在某些特定的技术或领域上还有待提升,需要更多的学习和实践。
# 业务
我的自我介绍
要技术方向是 React、React Native,同时也有 Vue 相关项目开发经验。
近几年主要使用 React Native 负责移动端 App 开发,熟悉 Android/iOS 双端开发流程,包括 BLE 蓝牙通信、Native Module 集成、第三方 SDK 接入以及 App 打包发布流程。
在项目中,我负责过智能硬件类 App 开发,例如 TROPS 智能足球项目,通过 React Native 连接足球脚环设备,实现运动数据采集、数据分析以及 AI 报告展示。
同时也负责一些通用组件、Hooks 封装以及性能优化工作,例如 BLE 通信能力抽象、SSE 流式数据处理、高频数据渲染优化等。
Web 方面熟悉 React、Vue3 、TypeScript、Vite 等技术栈,能够完成从需求分析、功能开发到上线维护的完整流程。
蓝牙单例+发布订阅模式的设计理念
蓝牙模块属于 App 全局能力,所以我会采用单例模式进行设计。
原因是如果每个页面都创建一个蓝牙实例,会导致多个页面同时管理蓝牙连接,容易出现重复连接、重复监听、状态不同步等问题。
所以我会创建一个全局 BLE Manager,在整个 App 生命周期中只维护一个蓝牙实例,统一负责:设备扫描、蓝牙连接、数据收发、状态监听、连接恢复
在数据处理方面,因为 BLE 返回的数据类型比较多,例如:设备状态、设备文件、固件升级数据、设备信息。所以不会在代码中大量使用 if else 判断,而是设计一个消息分发机制。
收到蓝牙数据后,会根据协议中的 type 或 command 标识,通过 Map 保存不同类型数据对应的处理方法。处理完成后,通过发布订阅模式通知业务层。
底层 BLE Manager 负责 emit 数据,页面或者 Hook 通过 listener 订阅自己关注的数据。
BLE Device ↓ BLE Manager(单例) ↓ Message Dispatcher(Map) ↓ EventEmitter(发布订阅) ↓ React Hook ↓ 页面业务1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21设计封装的想法
组件封装主要考虑复用性和扩展性。
如果多个页面存在相同结构,例如列表、卡片、弹窗等,我会抽离成公共组件。
对于结构固定、数据不同的场景,会通过 props 传递数据。
如果组件结构相同,但是内部内容需要高度自定义,会通过 children 或 render props 扩展。
AI编程的想法
我理解 AI 项目不仅仅是调用 AI 接口,而是一整套数据处理流程。
例如 TROPS 项目中:首先通过 BLE 设备采集球员训练数据,包括:速度、控球、爆发力、传射、耐力,然后上传到服务端,由 AI 模型进行分析,生成训练报告。
前端主要负责:
- 数据展示
- AI结果流式接收
- 状态管理
- 用户交互体验优化
例如 AI 报告生成时间较长,所以采用 SSE 实现流式输出,让用户可以实时看到 AI 生成内容,提高交互体验。
如何性能优化
React 渲染优化
首先减少无效渲染:
- 使用 React.memo 缓存组件
- 使用 useMemo 缓存复杂计算结果
- 使用 useCallback 保持函数引用稳定
但是不会所有地方都使用,因为过度优化可能增加维护成本,需要根据实际性能问题使用。
例如我的项目中,有实时数据展示和图表场景。如果每次收到数据都直接更新 React State,会导致频繁渲染,造成页面卡顿。所以采用 Buffer 缓冲机制:实时数据 → buffer缓存 → 固定时间间隔flush → 批量更新UI。同时图表更新不会重新创建整个实例,而是只更新变化的数据部分,提高渲染性能。
网络请求优化
项目中使用 React Query 进行接口管理。在 staleTime 时间范围内,再次进入页面不会重新请求,而是直接使用缓存数据。
静态资源优化
- 图片资源使用 CDN
- 图片压缩
- 第三方库按需加载
- 代码分包
减少首屏加载时间。
浏览器渲染优化
- 对于 scroll、resize 等高频事件:使用:防抖、节流减少事件触发次数。
大量数据列表如何渲染
虚拟列表,只渲染当前可视区域的数据。同时优化:item组件 React.memo、避免匿名函数、控制列表更新范围
如何避免表单重复提交
主要从前端和后端两个方向处理。
前端:提交后立即进入 loading 状态:禁止按钮点击、修改按钮状态
例如:点击提交 → loading=true → 接口返回 → 恢复按钮。同时对于连续点击场景,可以增加防抖处理。
后端:重要接口需要保证幂等性,例如订单提交,需要通过唯一请求ID避免重复创建。
如何判断组件进行无谓的渲染
可以通过:React DevTools Profiler
查看:哪些组件频繁render、render耗时、why-did-you-render分析组件为什么重新渲染。
常见原因:父组件重新渲染导致子组件更新、props引用变化、state设计不合理
PC端和移动端如何适配
主要通过响应式方案:
- viewport 设置移动端页面比例。
- CSS媒体查询根据屏幕宽度调整布局。
- Flex/Grid布局实现自适应。
- rem/vw适配解决不同屏幕尺寸。
- 移动端特殊处理:例如:iOS安全区域、键盘弹起、touch事件
后端分配权限后你会如何实现什么角色看什么内容
一般分两部分:
页面权限
用户登录后,后端返回:用户角色、动态路由、页面权限
前端根据权限:动态注册路由。
按钮权限
后端返回:前端通过权限指令控制
这样可以实现不同角色看到不同页面和操作。
你是如何进行技术栈选型的
后台管理系统:优先选择 Vue/React,生态成熟、组件库丰富、开发效率高
移动App:原生能力、双端维护会选择 React Native。
小程序、多端快速开发:uni-app、Taro
不会固定使用某一个技术,而是根据业务需求选择。
什么情况需要native module
- 蓝牙、NFC、相机、地图SDK、音视频SDK、厂商硬件SDK
- 以项目为例,足球脚环涉及固件升级功能,由于硬件厂商提供的是 Android/iOS 原生 SDK,所以需要通过 Native Module 封装,将原生能力提供给 React Native 调用。