Skip to content

前端核心知识点深度讲解

作者:青见春山

说明:本篇内容主要是对前端核心知识点的深度讲解,适合面试准备和知识复习。内容涵盖 JavaScript、HTML/CSS、网络安全、工程化等方面。

一、Canvas 图片压缩:格式支持与原理

1. Canvas 压缩支持哪些格式?

不是所有格式都能同等处理。 Canvas 的 toDataURLtoBlob 方法支持的输出格式:

方法支持格式说明
canvas.toDataURL(type, quality)image/pngimage/jpegimage/webpquality 0-1(仅 jpeg/webp 有效)
canvas.toBlob(callback, type, quality)image/pngimage/jpegimage/webp同上

关键点

  • image/png不支持 quality 参数,PNG 是无损格式,传了 quality 也不生效

  • image/jpeg:支持 quality,是压缩最常用的格式

  • image/webp:支持 quality,压缩率最好,但兼容性需注意

  • image/bmpimage/gifimage/tiff 等:Canvas 不直接支持输出

所以:Canvas 压缩本质上是对像素数据的重新编码,主要针对 JPEG 和 WebP 有效。

2. 压缩原理

JavaScript
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) }
          })
        })
      }
    })
  })
}

压缩本质是两步:

  1. 降低分辨率(缩小宽高 → 像素数减少)

  2. 降低编码质量(quality 参数 → JPEG/WebP 量化精度降低)


二、图片格式对比与 WebP 优势

1. 常见图片格式特点

格式压缩方式透明度动画色彩深度典型场景
PNG无损✅ Alpha❌(APNG 可)最高 32 位图标、截图、需要透明
JPEG有损24 位照片、复杂图像
GIF无损(LZW)1-bit8 位(256色)简单动画
WebP有损/无损✅ Alpha24/32 位全场景
AVIF有损/无损✅ Alpha最高 12 位下一代格式
SVG矢量无限图标、Logo、图形
BMP无压缩24 位极少使用

2. WebP 的核心优势

WebP 是 Google 开发的图片格式,在相同质量下文件体积更小。

对比vs JPEGvs 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. 基本用法

JavaScript
// 设置单个值
this.setData({ count: 1 })

// 设置多个值
this.setData({ name: '张三', age: 18 })

// 路径更新(只更新深层属性,减少传输量)
this.setData({ 'user.info.name': '新名字' })

// 数组元素更新
this.setData({ 'list[0].name': '修改第一个' })

2. 工作原理(重点)

Plain
逻辑线程 (App Service)          Native 层              渲染线程 (WebView)
      │                            │                        │
      │  this.setData({...})       │                        │
      │ ──→ JSON.stringify(data) ──→│──→ JSON.parse(data) ──→│
      │                            │     diff 比较          │→ 更新真实 DOM
      │                            │     最小化更新          │

每一次 **setData** 都是一次跨线程通信,包含:

  1. 逻辑层序列化 → 2. Native 转发 → 3. 渲染层反序列化 → 4. diff → 5. 更新

3. setData 的性能陷阱

JavaScript
// ❌ 错误:每次循环都 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. 现象

JavaScript
// 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 的原理

JavaScript
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 绑定的是原始对象地址

JavaScript
let state = reactive({ a: 1 })
// state 是 Proxy 对象的引用

let newState = reactive({ a: 2 })
state = newState  // ❌ state 变量指向新地址,但 Vue 追踪的还是旧 Proxy!
// 旧 Proxy 上的依赖不会被触发

丢失响应式的本质let 重新赋值只是改变了变量的指向,不是改变 Proxy 代理的原始对象。

响应式的本质是要用proxy进行代理

ref 的原理

JavaScript
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 的读写:

JavaScript
let count = ref(0)
// count 是 RefImpl 对象的引用(这个引用永远不变)

count.value = 1  // 改变的是 .value 属性,触发 setter → trigger
// ✅ 依赖被正确触发

count = ref(5)   // ❌ 这也会丢失(重新赋值了变量指向)
// 但正常使用不会这么做

3. 一句话总结

reactiveref
响应式粒度整个对象的属性访问/修改.value 的读写
赋值机制Proxy 的 set 拦截getter/setter 拦截
何时丢失变量重新指向新对象(= 重新赋值)变量重新指向新 ref(= 重新赋值)
为什么 ref 不易丢失使用时必须通过 .value 访问,天然保持对同一个 RefImpl 对象的引用

本质:两者都会因重新赋值丢失。但 reactive 的使用场景更容易触发重新赋值(函数返回值、解构等),而 ref 通过 .value 的二级访问机制,让开发者天然不会去替换引用本身。


六、defineProps 的响应式

1. 结论

defineProps** 接收到的值是响应式的,但不能直接解构。**

JavaScript
// ✅ 在模板中使用 —— 响应式
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+ 优化为只读的响应式引用)。

JavaScript
// Vue 内部简化逻辑
function defineProps(rawProps) {
  const props = reactive(rawProps)  // 整个 props 是响应式的
  return props
}

解构为什么会丢失?

JavaScript
const { msg } = props
// 等价于 const msg = props.msg(执行时刻的值)
// msg 变成了一个普通字符串,脱离了响应式系统

如何在解构时保持响应式?

JavaScript
// 方案一: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.msg

3. 为什么模板中不用 .value

因为 Vue 编译器在模板编译阶段自动对 props 属性做了 unref 处理:

Plain
模板 {{ msg }} → 编译为 _toDisplayString(unref(msg))

七、前端常用设计模式

1. 单例模式(Singleton)

概念:保证一个类只有一个实例,并提供全局访问点。

应用:全局状态管理(Vuex Store)、弹窗管理、数据库连接池

JavaScript
class Modal {
  static instance = null
  static getInstance() {
    if (!Modal.instance) {
      Modal.instance = new Modal()
    }
    return Modal.instance
  }
}

2. 观察者模式(Observer)

概念:对象间一对多依赖,当被观察者状态变化时,所有观察者自动收到通知。

应用:EventEmitter、Vue 的响应式系统、MobX

JavaScript
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

JavaScript
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 创建

JavaScript
function createButton(type) {
  switch(type) {
    case 'primary': return new PrimaryButton()
    case 'danger': return new DangerButton()
  }
}

5. 策略模式(Strategy)

概念:定义一系列算法,将每个算法封装起来,使它们可互换。

应用:表单校验、动画缓动函数、排序策略

JavaScript
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、缓存代理、访问控制

JavaScript
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 日志/权限

JavaScript
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 适配、旧接口兼容

JavaScript
// 适配 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 脚手架模板、构建工具流水线

JavaScript
// 小程序 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 设计模式与开发实践》没有官方免费在线版,但曾探的博客有不少相关文章

补充推荐的免费在线资源


九、requestAnimationFrame 到底是不是宏任务?

这是一个非常经典且容易混淆的问题。

1. 结论

requestAnimationFrame**(rAF)既不是宏任务,也不是微任务。它是一个独立的渲染回调,在浏览器渲染流程的特定阶段执行。**

2. 浏览器的事件循环完整流程

Plain
┌──────────────────────────────────┐
│       一个宏任务执行完             │
└──────────────┬───────────────────┘

┌──────────────────────────────────┐
│   执行所有微任务(Promise等)       │
│   微任务中产生的微任务也会执行      │
└──────────────┴───────────────────┘

┌──────────────────────────────────┐
│   🎯 执行 rAF 回调               │  ← 在这里!
└──────────────┬───────────────────┘

┌──────────────────────────────────┐
│   浏览器渲染(Layout → Paint)     │
└──────────────┬───────────────────┘

┌──────────────────────────────────┐
│   取下一个宏任务执行               │
└──────────────────────────────────┘

3. 详细解释

Plain
宏任务(Task) → 微任务(Microtask) → requestAnimationFrame → 渲染(Paint) → 宏任务...

rAF 的执行时机

  1. 一个宏任务执行完毕

  2. 清空微任务队列

  3. 检查 rAF 队列:如果当前时间点距离上次渲染 ≥ 16.67ms(60fps),则执行 rAF 回调

  4. 执行浏览器渲染(Layout → Composite → Paint)

  5. 取下一个宏任务

关键点

  • rAF 回调在渲染之前执行(所以叫"在下一次重绘前执行")

  • 不是宏任务队列的一部分,而是独立的渲染回调队列

  • 不是微任务(不会在宏任务后立即执行)

  • 不保证一定执行:如果浏览器不需要渲染(页面不可见、标签页在后台),rAF 不会被触发

4. 验证实验

JavaScript
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. 完整的任务执行顺序

JavaScript
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. requireimport 混用可能导致的 Bug

在小程序或 Node.js 环境中,混用 CommonJS 的 require 和 ES Module 的 import 可能导致以下问题:

问题原因
require** 拿到的不是 **default** 导出**importexport default 经过编译后,require 得到的是 { default: xxx } 这个整体对象,而不是直接拿到模块导出的值
循环引用行为不同require 是同步执行的,循环引用时拿到不完整的模块(部分导出);import 是声明提升 + 静态绑定,循环引用时通过"活绑定"可以拿到最终值
加载时序差异require 是运行时动态加载(可以写在 if 里),import 是编译阶段静态解析(必须在顶层);混用可能导致变量未定义或模块未加载
this** 指向不同**CommonJS 模块顶层 this 指向 module.exports;ES Module 顶层 thisundefined
编译产物不一致小程序的编译工具(如 Webpack/Vite)对两种模块系统的处理方式不同,混用时可能导致 tree-shaking 失效、代码体积膨胀、运行时报错

最典型的坑

JavaScript
// 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** 的完整流程**:

  1. 逻辑层调用 this.setData({ data: newData })

  2. 将数据序列化为 JSON 字符串(逻辑线程 → Native 层)

  3. Native 层传输给渲染线程

  4. 渲染线程反序列化 JSON

  5. 渲染线程更新 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,渲染和逻辑并行执行。

浏览器的跨线程通信机制

浏览器虽然主线程是单线程的,但可以通过以下方式实现跨线程/跨进程通信:

  1. Web Workers(真正的多线程)

    • 独立线程中运行 JS,不能操作 DOM

    • 通过 postMessage() / onmessage 通信(基于 结构化克隆算法 序列化数据)

    • 这其实和小程序的双线程通信模型非常类似

  2. 主线程上的异步通信

    • setTimeout / requestAnimationFrame —— 通过事件循环排队

    • Promise.then —— 微任务队列

    • 这些不是真正的跨线程通信,而是任务调度

  3. Service Worker / SharedWorker

    • 可以和主线程之间通过 postMessage 通信

    • Service Worker 还可以拦截网络请求、缓存资源

  4. 跨进程通信(浏览器架构层面):

    • 浏览器的渲染进程(Blink)和主进程(Browser Process)之间通过 IPC(进程间通信) 通信

    • 这是浏览器引擎层面的事,普通前端开发感知不到

总结:小程序的双线程通信本质上借鉴了 Web Worker 的 postMessage 模型,只是微信把它做成了强制性的架构约束。


4. 微信小程序登录流程

微信小程序的登录流程如下:

Plain
┌──────────┐          ┌──────────┐          ┌──────────┐
│  小程序    │          │ 微信服务器 │          │  你的后端  │
│ (客户端)   │          │ (wx.qq.com)│          │ (Server)  │
└────┬─────┘          └────┬─────┘          └────┬─────┘
     │                      │                      │
     │  1. wx.login()       │                      │
     │  ──────────────→     │                      │
     │                      │  返回 code            │
     │  ←──────────────     │                      │
     │                      │                      │
     │  2. 将 code 发送给后端  │                      │
     │  ──────────────────────────────────────→     │
     │                      │                      │
     │                      │  3. code + appid +    │
     │                      │     appsecret         │
     │                      │  ──────────────→     │
     │                      │                      │
     │                      │  返回 openid +        │
     │                      │  session_key          │
     │                      │  ←──────────────     │
     │                      │                      │
     │  4. 后端生成自定义      │                      │
     │     登录态(token)     │                      │
     │  ←──────────────────────────────────────     │
     │                      │                      │

关键步骤解释

  1. wx.login():调用微信 API,微信服务器返回一个临时 code(有效期5分钟)

  2. 发送 code 给后端:小程序把 code 发到你的服务器

  3. 后端换取 openid:后端用 code + appid + appsecret 调用微信的 jscode2session 接口,获取:

    • openid:用户唯一标识(同一用户在不同小程序 openid 不同)

    • session_key:服务端会话密钥,可用于校验、解密特定开放数据,不要下发给客户端;当前手机号能力通常由按钮返回动态 code,再由后端调用手机号接口换取,不应继续套用旧式 encryptedData 解密流程

  4. 后端生成 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 设备:拍摄的照片通常是 JPEGHEIF(取决于设备和 Android 版本)。通过微信 API 选择后通常返回 JPEG

  • 总结:通过微信小程序的 wx.chooseMedia()wx.chooseImage() 获取的图片,一般都是 JPEG 格式

前端转换图片格式

使用 Canvas API 进行格式转换:

JavaScript
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 压缩(最常用)

JavaScript
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)
  })
}

方法二:小程序中的压缩

JavaScript
// 微信小程序直接使用 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 中 refreactive 的响应式丢失问题

这是 Vue 3 面试高频题,两者的核心区别在于响应式追踪的粒度不同

ref 的响应式丢失

ref 包裹的是基本类型值,通过 .value 访问。当你解构 ref 时,会丢失响应式:

JavaScript
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 包裹的是对象,解构时同样会丢失响应式:

JavaScript
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

两者的响应式丢失机制对比

对比项refreactive
响应式丢失场景.value 当前值保存到普通变量后,变量不会随 ref 重绑定解构会断开“变量读取该属性”的联系
丢失原因取得的是当时的值或对象引用解构变量不再经过原对象该属性的 getter
属性是对象时得到的响应式对象仍能跟踪其内部变更得到的代理对象内部仍可响应,但原属性被整体替换时,解构变量不会自动指向新对象
保持属性联系保留 ref 本身,按需读取 .value使用 toRef() / toRefs()
本质区别ref.valuegetter/setter 拦截reactiveProxy 拦截整个对象

一个容易忽略的细节

JavaScript
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,因为:

  1. ref 在模板中自动解包,使用体验好

  2. ref 可以重新赋值(count.value = newVal),reactive 不能整体替换

  3. toRef / toRefs 工具更灵活