前端核心知识点深度讲解
说明:本篇内容主要是对前端核心知识点的深度讲解,适合面试准备和知识复习。内容涵盖 JavaScript、HTML/CSS、网络安全、工程化等方面。
一、Canvas 图片压缩:格式支持与原理
1. Canvas 压缩支持哪些格式?
不是所有格式都能同等处理。 Canvas 的 toDataURL 和 toBlob 方法支持的输出格式:
| 方法 | 支持格式 | 说明 |
|---|---|---|
canvas.toDataURL(type, quality) | image/png、image/jpeg、image/webp | quality 0-1(仅 jpeg/webp 有效) |
canvas.toBlob(callback, type, quality) | image/png、image/jpeg、image/webp | 同上 |
关键点:
image/png:不支持 quality 参数,PNG 是无损格式,传了 quality 也不生效image/jpeg:支持 quality,是压缩最常用的格式image/webp:支持 quality,压缩率最好,但兼容性需注意image/bmp、image/gif、image/tiff等:Canvas 不直接支持输出
所以:Canvas 压缩本质上是对像素数据的重新编码,主要针对 JPEG 和 WebP 有效。
2. 压缩原理
function compressImage(filePath, quality = 0.8) {
return new Promise((resolve) => {
wx.getImageInfo({
src: filePath,
success(info) {
const ctx = wx.createCanvasContext('compressCanvas')
// 将图片绘制到 canvas(指定目标尺寸实现缩放)
ctx.drawImage(filePath, 0, 0, info.width, info.height)
ctx.draw(false, () => {
wx.canvasToTempFilePath({
canvasId: 'compressCanvas',
fileType: 'jpg',
quality: quality,
success(res) { resolve(res.tempFilePath) }
})
})
}
})
})
}压缩本质是两步:
降低分辨率(缩小宽高 → 像素数减少)
降低编码质量(quality 参数 → JPEG/WebP 量化精度降低)
二、图片格式对比与 WebP 优势
1. 常见图片格式特点
| 格式 | 压缩方式 | 透明度 | 动画 | 色彩深度 | 典型场景 |
|---|---|---|---|---|---|
| PNG | 无损 | ✅ Alpha | ❌(APNG 可) | 最高 32 位 | 图标、截图、需要透明 |
| JPEG | 有损 | ❌ | ❌ | 24 位 | 照片、复杂图像 |
| GIF | 无损(LZW) | 1-bit | ✅ | 8 位(256色) | 简单动画 |
| WebP | 有损/无损 | ✅ Alpha | ✅ | 24/32 位 | 全场景 |
| AVIF | 有损/无损 | ✅ Alpha | ✅ | 最高 12 位 | 下一代格式 |
| SVG | 矢量 | ✅ | ✅ | 无限 | 图标、Logo、图形 |
| BMP | 无压缩 | ❌ | ❌ | 24 位 | 极少使用 |
2. WebP 的核心优势
WebP 是 Google 开发的图片格式,在相同质量下文件体积更小。
| 对比 | vs JPEG | vs PNG |
|---|---|---|
| 有损压缩 | 文件小 25-35% | — |
| 无损压缩 | — | 文件小 26% |
| 透明度 | 支持(JPEG 不支持) | 等同 |
| 动画 | 支持(JPEG 不支持) | 等同 |
WebP 为什么能做到更小?
有损编码:使用 VP8 视频编码中的帧内预测技术,比 JPEG 的 DCT 变换更高效
预测编码:利用相邻像素的相似性进行预测,只存储预测残差
自适应块大小:可使用 4×4 到 16×16 的可变宏块,比 JPEG 固定 8×8 更灵活
无损编码:使用 LZ77 + Huffman + 颜色变换的组合
3. 兼容性
WebP:主流浏览器支持率 >97%,微信小程序原生支持
AVIF:支持率 ~90%,部分旧浏览器不支持
三、小程序 setData 详解
1. 基本用法
// 设置单个值
this.setData({ count: 1 })
// 设置多个值
this.setData({ name: '张三', age: 18 })
// 路径更新(只更新深层属性,减少传输量)
this.setData({ 'user.info.name': '新名字' })
// 数组元素更新
this.setData({ 'list[0].name': '修改第一个' })2. 工作原理(重点)
逻辑线程 (App Service) Native 层 渲染线程 (WebView)
│ │ │
│ this.setData({...}) │ │
│ ──→ JSON.stringify(data) ──→│──→ JSON.parse(data) ──→│
│ │ diff 比较 │→ 更新真实 DOM
│ │ 最小化更新 │每一次 **setData** 都是一次跨线程通信,包含:
- 逻辑层序列化 → 2. Native 转发 → 3. 渲染层反序列化 → 4. diff → 5. 更新
3. setData 的性能陷阱
// ❌ 错误:每次循环都 setData(触发 N 次跨线程通信)
for (let i = 0; i < 100; i++) {
this.setData({ [`list[${i}]`]: data[i] })
}
// ✅ 正确:合并为一次
const map = {}
for (let i = 0; i < 100; i++) {
map[`list[${i}]`] = data[i]
}
this.setData(map)
// ❌ 错误:传递整个大对象
this.setData({ list: this.data.list }) // 整个数组重新 diff
// ✅ 正确:路径更新
this.setData({ 'list[5].name': '新值' }) // 只 diff 一个节点五、Vue 3:ref 保持响应式 vs reactive 丢失响应式
1. 现象
// reactive 丢失响应式
const state = reactive({ list: [1, 2, 3] })
// ⚠️ 直接替换数组,丢失响应式
state.list = [4, 5, 6] // ✅ 这个没问题
const obj = reactive({ a: 1, b: 2 })
// ⚠️ 整个替换对象
let obj2 = reactive({ a: 3, b: 4 })
obj = obj2 // ❌ 失去响应式!ref 变量被重新赋值
// ref 始终保持响应式
const count = ref(0)
count.value = 1 // ✅ 始终响应式2. 核心原理
关键区别在于「地址绑定」还是「值绑定」。
reactive 的原理
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
track(target, key) // 收集依赖
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
Reflect.set(target, key, value, receiver)
trigger(target, key) // 触发更新
return true
}
})
}reactive 返回的是一个 Proxy 对象。track/trigger 绑定的是原始对象地址。
let state = reactive({ a: 1 })
// state 是 Proxy 对象的引用
let newState = reactive({ a: 2 })
state = newState // ❌ state 变量指向新地址,但 Vue 追踪的还是旧 Proxy!
// 旧 Proxy 上的依赖不会被触发丢失响应式的本质:let 重新赋值只是改变了变量的指向,不是改变 Proxy 代理的原始对象。
响应式的本质是要用proxy进行代理
ref 的原理
function ref(value) {
return new RefImpl(value) // 包装成一个对象
}
class RefImpl {
constructor(value) {
this._value = value
}
get value() {
track(this, 'value') // 依赖收集
return this._value
}
set value(newVal) {
this._value = newVal
trigger(this, 'value') // 触发更新
}
}ref 是一个对象包装器,通过 getter/setter 拦截 .value 的读写:
let count = ref(0)
// count 是 RefImpl 对象的引用(这个引用永远不变)
count.value = 1 // 改变的是 .value 属性,触发 setter → trigger
// ✅ 依赖被正确触发
count = ref(5) // ❌ 这也会丢失(重新赋值了变量指向)
// 但正常使用不会这么做3. 一句话总结
| reactive | ref | |
|---|---|---|
| 响应式粒度 | 整个对象的属性访问/修改 | .value 的读写 |
| 赋值机制 | Proxy 的 set 拦截 | getter/setter 拦截 |
| 何时丢失 | 变量重新指向新对象(= 重新赋值) | 变量重新指向新 ref(= 重新赋值) |
| 为什么 ref 不易丢失 | — | 使用时必须通过 .value 访问,天然保持对同一个 RefImpl 对象的引用 |
本质:两者都会因重新赋值丢失。但 reactive 的使用场景更容易触发重新赋值(函数返回值、解构等),而 ref 通过 .value 的二级访问机制,让开发者天然不会去替换引用本身。
六、defineProps 的响应式
1. 结论
defineProps** 接收到的值是响应式的,但不能直接解构。**
// ✅ 在模板中使用 —— 响应式
const props = defineProps({ msg: String })
// <template>{{ msg }}</template> ✅ 响应式
// ✅ 在 JS 中通过 props.xxx 访问 —— 响应式
watch(() => props.msg, (newVal) => { })
// ❌ 直接解构 —— 丢失响应式!
const { msg } = props // msg 只是一个普通值的快照2. 为什么?
defineProps 返回的是一个 reactive 对象(Vue 3.5 之前是 reactive,3.5+ 优化为只读的响应式引用)。
// Vue 内部简化逻辑
function defineProps(rawProps) {
const props = reactive(rawProps) // 整个 props 是响应式的
return props
}解构为什么会丢失?
const { msg } = props
// 等价于 const msg = props.msg(执行时刻的值)
// msg 变成了一个普通字符串,脱离了响应式系统如何在解构时保持响应式?
// 方案一:toRefs(Vue 3.5 之前推荐)
import { toRefs } from 'vue'
const { msg } = toRefs(props)
// msg 是 RefImpl,通过 msg.value 访问
// 方案二:defineProps + toRefs(Vue 3.5+ 推荐)
const { msg } = defineProps({ msg: String }) // 3.5+ 编译器优化
// 但最稳妥还是用 toRefs
// 方案三:不解构
const props = defineProps({ msg: String })
// 直接用 props.msg3. 为什么模板中不用 .value?
因为 Vue 编译器在模板编译阶段自动对 props 属性做了 unref 处理:
模板 {{ msg }} → 编译为 _toDisplayString(unref(msg))七、前端常用设计模式
1. 单例模式(Singleton)
概念:保证一个类只有一个实例,并提供全局访问点。
应用:全局状态管理(Vuex Store)、弹窗管理、数据库连接池
class Modal {
static instance = null
static getInstance() {
if (!Modal.instance) {
Modal.instance = new Modal()
}
return Modal.instance
}
}2. 观察者模式(Observer)
概念:对象间一对多依赖,当被观察者状态变化时,所有观察者自动收到通知。
应用:EventEmitter、Vue 的响应式系统、MobX
class EventEmitter {
events = {}
on(event, fn) {
(this.events[event] = this.events[event] || []).push(fn)
}
emit(event, ...args) {
(this.events[event] || []).forEach(fn => fn(...args))
}
}与发布订阅的区别:观察者模式中被观察者直接通知观察者;发布订阅模式通过中间的调度中心(事件总线)解耦。
3. 发布订阅模式(Pub-Sub)
概念:发布者和订阅者完全解耦,通过事件中心通信。
应用:Vue Event Bus、RxJS、Redux 的 dispatch/subscribe
class PubSub {
subscribers = {}
subscribe(event, fn) {
(this.subscribers[event] = this.subscribers[event] || []).push(fn)
}
publish(event, data) {
(this.subscribers[event] || []).forEach(fn => fn(data))
}
}4. 工厂模式(Factory)
概念:提供一个创建对象的接口,由子类决定实例化哪个类。
应用:组件工厂、不同平台的 API 适配器、VNode 创建
function createButton(type) {
switch(type) {
case 'primary': return new PrimaryButton()
case 'danger': return new DangerButton()
}
}5. 策略模式(Strategy)
概念:定义一系列算法,将每个算法封装起来,使它们可互换。
应用:表单校验、动画缓动函数、排序策略
const strategies = {
required: (val) => val !== '' || '必填',
minLength: (val, len) => val.length >= len || `最少${len}位`,
email: (val) => /^\S+@\S+$/.test(val) || '邮箱格式错误'
}
function validate(value, rules) {
return rules.map(rule => {
const [strategy, ...params] = rule.split(':')
return strategies[strategy](value, ...params)
}).find(result => result !== true)
}6. 代理模式(Proxy)
概念:为对象提供一个代理以控制对它的访问。
应用:Vue 3 响应式、ES6 Proxy、缓存代理、访问控制
const cache = new Map()
const proxy = new Proxy(target, {
get(target, key) {
if (cache.has(key)) return cache.get(key)
const result = Reflect.get(target, key)
cache.set(key, result)
return result
}
})7. 装饰器模式(Decorator)
概念:动态地给对象添加职责,比继承更灵活。
应用:TypeScript 装饰器、React HOC、AOP 日志/权限
function withLogging(fn) {
return function(...args) {
console.log(`调用 ${fn.name},参数:`, args)
const result = fn.apply(this, args)
console.log(`返回:`, result)
return result
}
}
const add = (a, b) => a + b
const loggedAdd = withLogging(add)
loggedAdd(1, 2) // 日志输出8. 适配器模式(Adapter)
概念:将一个类的接口转换成客户端期望的另一个接口。
应用:Axios 适配浏览器/Node、跨端框架的平台 API 适配、旧接口兼容
// 适配 wx.request 到标准 fetch 接口
function wxFetch(url, options) {
return new Promise((resolve, reject) => {
wx.request({
url, ...options,
success: resolve,
fail: reject
})
})
}9. 组合模式(Composite)
概念:将对象组合成树形结构,统一处理单个对象和组合对象。
应用:DOM 树、文件系统、Vue/React 组件树、菜单组件
10. 模板方法模式(Template Method)
概念:定义算法骨架,将某些步骤延迟到子类实现。
应用:小程序 Page/Component 生命周期、Vue 脚手架模板、构建工具流水线
// 小程序 Page 本质就是模板方法
Page({
onLoad() { this.init() }, // 框架调用
onShow() { this.onResume() }, // 框架调用
init() { /* 子类实现 */ },
onResume() { /* 子类实现 */ }
})设计模式速查表
| 模式 | 核心思想 | 关键词 | 典型应用 |
|---|---|---|---|
| 单例 | 唯一实例 | 全局唯一 | Store、弹窗 |
| 观察者 | 直接通知 | 一对多 | Vue 响应式 |
| 发布订阅 | 间接通信 | 解耦 | EventBus、Redux |
| 工厂 | 创建封装 | 不暴露 new | 组件创建 |
| 策略 | 算法替换 | 可互换 | 表单校验 |
| 代理 | 访问控制 | 拦截 | Vue 3 Proxy |
| 装饰器 | 动态增强 | 不改原对象 | HOC、AOP |
| 适配器 | 接口转换 | 兼容 | wx.request 封装 |
八、推荐书籍在线阅读链接
入门级
| 书名 | 链接 |
|---|---|
| 《JavaScript 高级程序设计》(红宝书) | 在线版:https://www.zhjavascript.com/ (中文翻译,非官方) |
| 《你不知道的 JavaScript》(上/中/下) | GitHub 中文版:https://github.com/getify/You-Dont-Know-JS(英文原版免费) |
| 《JavaScript 语言精粹》(蝴蝶书) | 没有官方免费在线版,部分在线资源可搜索「JavaScript 语言精粹 在线阅读」 |
进阶级
| 书名 | 链接 |
|---|---|
| 《ES6 入门教程》阮一峰 | 免费在线版:https://es6.ruanyifeng.com/ |
| 《JavaScript 设计模式与开发实践》 | 没有官方免费在线版,但曾探的博客有不少相关文章 |
补充推荐的免费在线资源:
https://chinese.freecodecamp.org/ — freeCodeCamp 中文版
https://developer.mozilla.org/zh-CN/ — MDN 中文文档(最权威的前端参考)
九、requestAnimationFrame 到底是不是宏任务?
这是一个非常经典且容易混淆的问题。
1. 结论
requestAnimationFrame**(rAF)既不是宏任务,也不是微任务。它是一个独立的渲染回调,在浏览器渲染流程的特定阶段执行。**
2. 浏览器的事件循环完整流程
┌──────────────────────────────────┐
│ 一个宏任务执行完 │
└──────────────┬───────────────────┘
↓
┌──────────────────────────────────┐
│ 执行所有微任务(Promise等) │
│ 微任务中产生的微任务也会执行 │
└──────────────┴───────────────────┘
↓
┌──────────────────────────────────┐
│ 🎯 执行 rAF 回调 │ ← 在这里!
└──────────────┬───────────────────┘
↓
┌──────────────────────────────────┐
│ 浏览器渲染(Layout → Paint) │
└──────────────┬───────────────────┘
↓
┌──────────────────────────────────┐
│ 取下一个宏任务执行 │
└──────────────────────────────────┘3. 详细解释
宏任务(Task) → 微任务(Microtask) → requestAnimationFrame → 渲染(Paint) → 宏任务...rAF 的执行时机:
一个宏任务执行完毕
清空微任务队列
检查 rAF 队列:如果当前时间点距离上次渲染 ≥ 16.67ms(60fps),则执行 rAF 回调
执行浏览器渲染(Layout → Composite → Paint)
取下一个宏任务
关键点:
rAF 回调在渲染之前执行(所以叫"在下一次重绘前执行")
它不是宏任务队列的一部分,而是独立的渲染回调队列
它不是微任务(不会在宏任务后立即执行)
它不保证一定执行:如果浏览器不需要渲染(页面不可见、标签页在后台),rAF 不会被触发
4. 验证实验
setTimeout(() => console.log('1. 宏任务 setTimeout'), 0)
Promise.resolve().then(() => console.log('2. 微任务 Promise'))
requestAnimationFrame(() => console.log('3. rAF'))
// 输出顺序:
// 2. 微任务 Promise ← 微任务先执行
// 3. rAF ← rAF 在渲染前执行
// 1. 宏任务 setTimeout ← 下一个宏任务5. 为什么 rAF 不算宏任务?
| 特征 | 宏任务 | rAF |
|---|---|---|
| 执行时机 | 事件循环的每一轮取一个 | 渲染前批量执行 |
| 是否保证执行 | 是 | 否(页面不可见时跳过) |
| 执行频率 | 每个任务执行一次 | 最高 60fps(跟随屏幕刷新率) |
| 队列机制 | 标准任务队列 | 独立的渲染回调队列 |
| 调度方式 | 事件驱动(IO、Timer) | 时间驱动(帧预算) |
6. 完整的任务执行顺序
console.log('同步代码') // 1. 同步(宏任务主体)
setTimeout(() => {
console.log('宏任务: setTimeout') // 5. 宏任务
}, 0)
Promise.resolve().then(() => {
console.log('微任务: Promise') // 2. 微任务
})
requestAnimationFrame(() => {
console.log('rAF') // 3. rAF(渲染前)
})
// 输出:
// 同步代码
// 微任务: Promise
// rAF
// 宏任务: setTimeout(下一个事件循环轮次)总结:requestAnimationFrame 是浏览器渲染管线的一部分,属于渲染回调,在微任务之后、渲染之前执行。它处于宏任务和微任务之间的"第三种位置",所以在面试中应该回答:rAF 既不是宏任务也不是微任务,它是在浏览器渲染前执行的渲染回调,拥有独立的队列机制。
1. require 和 import 混用可能导致的 Bug
在小程序或 Node.js 环境中,混用 CommonJS 的 require 和 ES Module 的 import 可能导致以下问题:
| 问题 | 原因 |
|---|---|
require** 拿到的不是 **default** 导出** | import 的 export default 经过编译后,require 得到的是 { default: xxx } 这个整体对象,而不是直接拿到模块导出的值 |
| 循环引用行为不同 | require 是同步执行的,循环引用时拿到不完整的模块(部分导出);import 是声明提升 + 静态绑定,循环引用时通过"活绑定"可以拿到最终值 |
| 加载时序差异 | require 是运行时动态加载(可以写在 if 里),import 是编译阶段静态解析(必须在顶层);混用可能导致变量未定义或模块未加载 |
this** 指向不同** | CommonJS 模块顶层 this 指向 module.exports;ES Module 顶层 this 是 undefined |
| 编译产物不一致 | 小程序的编译工具(如 Webpack/Vite)对两种模块系统的处理方式不同,混用时可能导致 tree-shaking 失效、代码体积膨胀、运行时报错 |
最典型的坑:
// a.js 使用 export default
export default { name: 'test' }
// b.js 混用 require
const a = require('./a')
console.log(a.name) // undefined!实际是 a.default.name建议:在一个项目中统一使用一种模块系统。现代项目统一用 import/export 即可。
2. 小程序双线程模型的缺点与 setData 性能瓶颈
你画的架构图很清晰。核心问题在于:
setData** 的完整流程**:
逻辑层调用
this.setData({ data: newData })将数据序列化为 JSON 字符串(逻辑线程 → Native 层)
Native 层传输给渲染线程
渲染线程反序列化 JSON
渲染线程更新 Virtual DOM → diff → 更新真实 DOM
性能瓶颈:
每次
setData都要序列化+反序列化,数据量大时耗时严重逻辑层和渲染层各自维护一份数据副本,内存翻倍
频繁
setData(如列表滑动、动画)会产生大量跨线程通信,阻塞渲染帧率
优化手段:
setData的 key-path 路径更新(只传变化的部分)合并多次
setData为一次使用
this.setData的第二个参数(回调)来知道渲染完成时间
3. 浏览器没有双线程架构的问题 & 浏览器如何跨线程通信
浏览器没有双线程的问题
浏览器的 JS 和 DOM 在同一个线程(主线程)上,这意味着:
| 问题 | 说明 |
|---|---|
| JS 执行阻塞渲染 | 执行一段耗时 JS 时,页面完全无法响应,用户会感到"卡死" |
| DOM 操作阻塞 JS | 频繁读取 DOM(如 offsetHeight)会触发强制回流(reflow),JS 线程暂停等待渲染完成 |
| 长任务掉帧 | 一个超过 16.6ms 的任务就会导致掉帧,超过 50ms 就会被标记为"长任务" |
| 安全风险 | JS 可以直接操作 DOM,理论上可以做恶意篡改(如 XSS 注入后篡改页面内容) |
小程序双线程的设计恰好解决了这些问题:JS 不能直接操作 DOM,渲染和逻辑并行执行。
浏览器的跨线程通信机制
浏览器虽然主线程是单线程的,但可以通过以下方式实现跨线程/跨进程通信:
Web Workers(真正的多线程):
独立线程中运行 JS,不能操作 DOM
通过
postMessage()/onmessage通信(基于 结构化克隆算法 序列化数据)这其实和小程序的双线程通信模型非常类似!
主线程上的异步通信:
setTimeout/requestAnimationFrame—— 通过事件循环排队Promise.then—— 微任务队列这些不是真正的跨线程通信,而是任务调度
Service Worker / SharedWorker:
可以和主线程之间通过
postMessage通信Service Worker 还可以拦截网络请求、缓存资源
跨进程通信(浏览器架构层面):
浏览器的渲染进程(Blink)和主进程(Browser Process)之间通过 IPC(进程间通信) 通信
这是浏览器引擎层面的事,普通前端开发感知不到
总结:小程序的双线程通信本质上借鉴了 Web Worker 的 postMessage 模型,只是微信把它做成了强制性的架构约束。
4. 微信小程序登录流程
微信小程序的登录流程如下:
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 小程序 │ │ 微信服务器 │ │ 你的后端 │
│ (客户端) │ │ (wx.qq.com)│ │ (Server) │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
│ 1. wx.login() │ │
│ ──────────────→ │ │
│ │ 返回 code │
│ ←────────────── │ │
│ │ │
│ 2. 将 code 发送给后端 │ │
│ ──────────────────────────────────────→ │
│ │ │
│ │ 3. code + appid + │
│ │ appsecret │
│ │ ──────────────→ │
│ │ │
│ │ 返回 openid + │
│ │ session_key │
│ │ ←────────────── │
│ │ │
│ 4. 后端生成自定义 │ │
│ 登录态(token) │ │
│ ←────────────────────────────────────── │
│ │ │关键步骤解释:
wx.login():调用微信 API,微信服务器返回一个临时code(有效期5分钟)发送 code 给后端:小程序把
code发到你的服务器后端换取 openid:后端用
code+appid+appsecret调用微信的jscode2session接口,获取:openid:用户唯一标识(同一用户在不同小程序 openid 不同)session_key:服务端会话密钥,可用于校验、解密特定开放数据,不要下发给客户端;当前手机号能力通常由按钮返回动态 code,再由后端调用手机号接口换取,不应继续套用旧式 encryptedData 解密流程
后端生成 token:后端用自己的 JWT/Session 机制生成登录态返回给客户端
补充:
登录身份与头像昵称授权是两件事:
wx.login()只取得临时登录凭证,不会自动返回用户资料。头像昵称等能力变化较快,应按当前开放能力文档采用对应组件/API,不能把wx.getUserProfile()固定写成所有版本的推荐方案如果需要获取手机号,需要用
getPhoneNumber组件按钮,后端通过手机号授权 code 换取手机号
5. JPG 与 JPEG 的区别 & 前端图片格式转换/压缩
JPG 和 JPEG 的区别
技术上完全相同。它们是同一种格式(Joint Photographic Experts Group)。
| 项目 | 说明 |
|---|---|
.jpg | 早期 Windows 系统文件扩展名只支持 3 位,所以把 .jpeg 缩写为 .jpg |
.jpeg | 标准扩展名,支持 4 位 |
两者在编码、压缩算法、色彩空间上没有任何区别,只是扩展名长度不同。
拍照/相册选择的图片格式
iOS 设备:拍摄的照片通常是 HEIC/HEIF 格式(iOS 11+),压缩率比 JPEG 高 50%,但部分场景会用 JPEG。通过微信
wx.chooseMedia()选择时,iOS 会返回 JPEG(微信做了格式转换)Android 设备:拍摄的照片通常是 JPEG 或 HEIF(取决于设备和 Android 版本)。通过微信 API 选择后通常返回 JPEG
总结:通过微信小程序的
wx.chooseMedia()或wx.chooseImage()获取的图片,一般都是 JPEG 格式
前端转换图片格式
使用 Canvas API 进行格式转换:
function convertImage(file, targetFormat = 'image/jpeg', quality = 0.92) {
return new Promise((resolve) => {
const img = new Image()
img.onload = () => {
const canvas = document.createElement('canvas')
canvas.width = img.width
canvas.height = img.height
const ctx = canvas.getContext('2d')
ctx.drawImage(img, 0, 0)
canvas.toBlob((blob) => {
resolve(blob)
}, targetFormat, quality)
}
img.src = URL.createObjectURL(file)
})
}
// 使用:将 PNG 转为 JPEG
const jpegBlob = await convertImage(pngFile, 'image/jpeg', 0.8)图片压缩方法
方法一:Canvas 压缩(最常用)
function compressImage(file, maxWidth = 1080, quality = 0.7) {
return new Promise((resolve) => {
const img = new Image()
img.onload = () => {
let { width, height } = img
if (width > maxWidth) {
height = (height * maxWidth) / width
width = maxWidth
}
const canvas = document.createElement('canvas')
canvas.width = width
canvas.height = height
const ctx = canvas.getContext('2d')
ctx.drawImage(img, 0, 0, width, height)
canvas.toBlob((blob) => resolve(blob), 'image/jpeg', quality)
}
img.src = URL.createObjectURL(file)
})
}方法二:小程序中的压缩
// 微信小程序直接使用 wx.compressImage
wx.compressImage({
src: 'tempFilePath',
quality: 50, // 压缩质量 0-100
success(res) {
console.log(res.tempFilePath) // 压缩后的图片
}
})方法三:使用第三方库
browser-image-compression(浏览器端,基于 Web Worker,不阻塞主线程)
sharp(Node.js 服务端,基于 libvips,性能极高)
7. Vue 3 中 ref 和 reactive 的响应式丢失问题
这是 Vue 3 面试高频题,两者的核心区别在于响应式追踪的粒度不同:
ref 的响应式丢失
ref 包裹的是基本类型值,通过 .value 访问。当你解构 ref 时,会丢失响应式:
import { ref, computed } from 'vue'
const count = ref(0)
// ❌ 响应式丢失
let { value } = count // value 是 0(一个普通数字,不是响应式的)
value++ // 不会触发视图更新
// ✅ 保持响应式
const doubleCount = computed(() => count.value * 2) // 这不会丢,因为闭包引用了 count为什么丢失? 因为 JavaScript 基本类型是值传递的,解构时只是复制了 count.value 的当前值(一个数字 0),之后 count.value 变化和这个数字就无关了。
reactive 的响应式丢失
reactive 包裹的是对象,解构时同样会丢失响应式:
import { reactive } from 'vue'
const state = reactive({ name: '张三', age: 25 })
// ❌ 响应式丢失
const { name, age } = state // name 和 age 只是普通字符串和数字
name = '李四' // 不会触发视图更新
// ✅ 保持响应式 —— 方式1:toRefs
import { toRefs } from 'vue'
const { name, age } = toRefs(state) // name 和 age 现在是 ref
name.value = '李四' // ✅ 触发更新
// ✅ 保持响应式 —— 方式2:整体使用
// 不要解构,直接用 state.name、state.age两者的响应式丢失机制对比
| 对比项 | ref | reactive |
|---|---|---|
| 响应式丢失场景 | 把 .value 当前值保存到普通变量后,变量不会随 ref 重绑定 | 解构会断开“变量读取该属性”的联系 |
| 丢失原因 | 取得的是当时的值或对象引用 | 解构变量不再经过原对象该属性的 getter |
| 属性是对象时 | 得到的响应式对象仍能跟踪其内部变更 | 得到的代理对象内部仍可响应,但原属性被整体替换时,解构变量不会自动指向新对象 |
| 保持属性联系 | 保留 ref 本身,按需读取 .value | 使用 toRef() / toRefs() |
| 本质区别 | ref 用 .value 的 getter/setter 拦截 | reactive 用 Proxy 拦截整个对象 |
一个容易忽略的细节
const state = reactive({ user: { name: '张三' } })
// 解构 user 对象 —— 仍然保持响应式!
const { user } = state // user 仍是 state.user 的引用
user.name = '李四' // ✅ 触发更新(因为 user 是对象引用,没有断开)
// 但如果从嵌套对象解构基本类型:
const { name } = state.user // name = '张三'(字符串值)
name = '王五' // ❌ 不触发更新因此,解构总会断开变量与“原对象属性槽位”的联系。若解构值本身是响应式代理,继续修改它的内部属性仍可触发响应;但如果之后执行 state.user = anotherUser,旧的 user 变量不会自动改为新对象。需要持续跟踪原属性时应使用 toRef / toRefs。
实践建议
在 <script setup> 中,推荐使用 ref 而非 reactive,因为:
ref在模板中自动解包,使用体验好ref可以重新赋值(count.value = newVal),reactive不能整体替换toRef/toRefs工具更灵活