阅读视图

发现新文章,点击刷新页面。

多个组件库混用导致JS爆炸?看我如何瘦身70%!

大家好,我是小杨,一个被 "Bundle Size过大" 折磨了6年的前端老鸟。最近接手一个项目,发现打包后的JS居然有5MB+ !一查原因:同时用了Element UI、Ant Design、Vant三个组件库!今天就来分享我的极限压缩实战经验


1. 先看问题有多严重

webpack-bundle-analyzer分析打包结果,发现:

  • 重复的组件:三个库都有Button、Modal
  • 冗余的工具函数:每个库都自带utils
  • 未按需加载:全量引入了所有组件

fake-url-for-example.com/bundle-anal…
(示意图:各种库的代码像俄罗斯方块一样堆叠)


2. 我的七步瘦身大法

✅ 第一步:按需加载(立减50%)

// 错误写法(全量引入)  
import ElementUI from 'element-ui'  

// 正确写法(按需引入)  
import { Button, Select } from 'element-ui'  

效果:从500KB → 250KB

✅ 第二步:共用同版本依赖(解决重复打包)

// webpack配置  
resolve: {  
  alias: {  
    'moment': path.resolve('./node_modules/moment'),  
    'lodash': path.resolve('./node_modules/lodash')  
  }  
}  

原理:强制所有库用同一个版本的moment/lodash

✅ 第三步:开启Gzip/Brotli压缩(再减60%)

# Nginx配置  
gzip on;  
gzip_types application/javascript;  
brotli on;  

效果:2MB → 800KB

✅ 第四步:抽离公共代码(CommonsChunkPlugin)

// webpack 4+  
optimization: {  
  splitChunks: {  
    chunks: 'all'  
  }  
}  

✅ 第五步:动态导入(懒加载)

// 非首屏组件改用动态导入  
const HeavyComponent = () => import('@/components/HeavyComponent')  

✅ 第六步:移除SourceMap(生产环境)

// vue.config.js  
productionSourceMap: false  

✅ 第七步:终极杀招——换轻量库

  • Day.js替代Moment.js(从200KB → 2KB)
  • lodash-es按需导入

3. 我的翻车现场

事故1:某次优化后页面白屏
原因:误删了公共依赖的polyfill
解法

// 显式声明核心依赖  
import 'core-js/stable'  
import 'regenerator-runtime/runtime'  

事故2:IE11报错
原因:用了Brotli压缩但IE不支持
解法

# Nginx回退方案  
brotli_static off;  
gzip_static on;  

4. 效果对比

优化阶段 JS体积 首屏加载时间
原始状态 5.2MB 8.7s
按需加载 2.8MB 5.2s
公共代码抽离 1.9MB 3.8s
Gzip压缩后 750KB 2.1s

5. 写给架构师的建议

  1. 设计阶段选型:避免混用同类库(比如同时用AntD和Element)

  2. 制定规范

    • 所有组件必须按需引入
    • 工具库统一版本
  3. 监控机制

    // 打包大小阈值警告  
    performance: {  
      maxEntrypointSize: 500000,  
      maxAssetSize: 500000  
    }  
    

6. 高级技巧:组件库CDN化

<!-- 把Vue/ElementUI等移出Bundle -->  
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.min.js"></script>  
<script src="https://cdn.jsdelivr.net/npm/element-ui@2.15.6/lib/index.min.js"></script>  

注意:要配置externals避免重复打包

// webpack配置  
externals: {  
  'vue': 'Vue',  
  'element-ui': 'ELEMENT'  
}  

最后一句忠告

"Bundle Size优化就像减肥——快速瘦身容易反弹,长期控制才是王道!"

⭐  写在最后

请大家不吝赐教,在下方评论或者私信我,十分感谢🙏🙏🙏.

✅ 认为我某个部分的设计过于繁琐,有更加简单或者更高逼格的封装方式

✅ 认为我部分代码过于老旧,可以提供新的API或最新语法

✅ 对于文章中部分内容不理解

✅ 解答我文章中一些疑问

✅ 认为某些交互,功能需要优化,发现BUG

✅ 想要添加新功能,对于整体的设计,外观有更好的建议

✅ 一起探讨技术加qq交流群:906392632

最后感谢各位的耐心观看,既然都到这了,点个 👍赞再走吧!

Vue懒加载全揭秘:从2.x到3.0,我是这样优化首屏速度的!

大家好,我是小杨,一个和Vue相爱相杀6年的老司机。今天要聊的是个既基础又容易踩坑的话题——Vue中的懒加载。最近团队新人问我:"小杨哥,Vue 2.0是不是不能实现懒加载啊?" 我当场就笑了...

1. 先破谣言:Vue 2.x当然能懒加载!

真相:Vue 2.x通过动态import+异步组件完美支持懒加载,这是ES6特性,和Vue版本无关!

// Vue 2.x 标准写法
const MyComponent = () => import('./MyComponent.vue')

2. 我的性能优化实战

场景1:路由懒加载(最常用)

// router.js
const routes = [
  {
    path: '/dashboard',
    component: () => import('@/views/Dashboard.vue') // 关键在这!
  }
]

效果:首次加载只下载当前路由的代码,其他路由等访问时再加载

场景2:组件级懒加载

// 父组件中
export default {
  components: {
    'my-heavy-component': () => import('./HeavyComponent.vue')
  }
}

3. 原理深挖(看过源码的来)

Vue 2.x的懒加载核心是:

  1. webpack的代码分割(生成单独的chunk)
  2. Vue的异步组件工厂函数
  3. 底层使用Promise

源码关键点(简化版):

// vue/src/core/vdom/async-component.js
function resolveAsyncComponent(
  factory: Function,
  baseCtor: Class<Component>
): Class<Component> | void {
  if (isPromise(factory.resolved)) {
    return factory.resolved
  }
  const resolve = (res: Object | Class<Component>) => {
    factory.resolved = ensureCtor(res, baseCtor)
    if (!sync) {
      forceRender(false)
    }
  }
  const res = factory(resolve, reject) // 这里执行import()
  if (isObject(res)) {
    if (isPromise(res)) {
      res.then(resolve, reject)
    }
  }
}

4. Vue 2.x懒加载的三大坑

坑① 魔法注释失效

// 有时候webpack魔法注释不生效
const Foo = () => import(/* webpackChunkName: "my-chunk" */ './Foo.vue')

解法:检查babel配置是否转译了注释

坑② 预加载时机难控

// 可能提前加载非必要资源
const Foo = () => import('./Foo.vue' /* webpackPrefetch: true */)

解法:慎用prefetch,优先用preload

坑③ 错误处理缺失

// 网络出错会白屏
const Foo = () => import('./Foo.vue')

解法:加错误边界

const Foo = () => ({
  component: import('./Foo.vue'),
  loading: LoadingComponent,
  error: ErrorComponent,
  delay: 200,
  timeout: 3000
})

5. Vue 3的超级升级

Vue 3的defineAsyncComponent更强大:

import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent({
  loader: () => import('./Foo.vue'),
  delay: 200, // 延迟显示loading
  timeout: 3000, // 超时处理
  suspensible: false // 是否配合Suspense使用
})

6. 性能对比实测

我的一个项目优化数据:

方案 首屏体积 LCP时间
传统加载 1.2MB 2.8s
Vue 2.x懒加载 420KB 1.4s
Vue 3懒加载 380KB 1.2s

7. 写给新手的建议

  1. 路由必须懒加载:这是性价比最高的优化
  2. 大组件才懒加载:小于30KB的组件没必要
  3. 注意加载状态:一定要加loading效果
  4. 生产环境验证:记得检查chunk是否真的拆分

8. 高级玩法:动态懒加载

我在后台管理系统这样用:

// 根据用户权限动态加载模块
const getAdminComponent = () => {
  return user.isSuperAdmin 
    ? import('./SuperAdmin.vue')
    : import('./NormalAdmin.vue')
}

最后说句大实话

"懒加载不是银弹,用不好反而会降低用户体验" —— 这是我在性能优化分享会上反复强调的。

⭐  写在最后

请大家不吝赐教,在下方评论或者私信我,十分感谢🙏🙏🙏.

✅ 认为我某个部分的设计过于繁琐,有更加简单或者更高逼格的封装方式

✅ 认为我部分代码过于老旧,可以提供新的API或最新语法

✅ 对于文章中部分内容不理解

✅ 解答我文章中一些疑问

✅ 认为某些交互,功能需要优化,发现BUG

✅ 想要添加新功能,对于整体的设计,外观有更好的建议

✅ 一起探讨技术加qq交流群:906392632

最后感谢各位的耐心观看,既然都到这了,点个 👍赞再走吧!

include和exclude傻傻分不清?3分钟让你彻底搞懂!

大家好,我是小杨,一个写了6年前端的老司机。今天咱们来聊聊开发中经常遇到的两个概念——includeexclude。别看它们长得像,用起来可是天差地别!

1. 先看生活化的例子

想象你在准备一场聚会:

  • include:邀请名单(明确指定谁来)
  • exclude:黑名单(明确指定谁不能来)

2. 代码中的经典应用

场景1:路由守卫(Vue Router)

// 只对/about和/contact路由生效
{
  path: '/',
  component: Home,
  meta: { requiresAuth: true },
  include: ['/about', '/contact'] // 白名单模式
}

// 对所有路由生效,除了/login
{
  path: '/',
  component: Home,
  meta: { requiresAuth: true },
  exclude: ['/login'] // 黑名单模式
}

场景2:Webpack配置

// 只处理src目录下的js文件
{
  test: /.js$/,
  include: path.resolve(__dirname, 'src'), // 白名单
  loader: 'babel-loader'
}

// 处理所有js文件,除了node_modules
{
  test: /.js$/,
  exclude: /node_modules/, // 黑名单
  loader: 'babel-loader'
}

3. 我踩过的血泪坑

案例1:有次我写了个权限中间件:

// 错误写法!
const allowedRoutes = ['/home', '/profile']
if (!allowedRoutes.includes(req.path)) {
  return res.status(403).send('无权访问')
}

结果把登录页也拦截了!应该用exclude:

const blockedRoutes = ['/admin']
if (blockedRoutes.includes(req.path)) {
  return res.status(403).send('无权访问')
}

案例2:Webpack打包时不小心:

{
  test: /.css$/,
  exclude: /styles/, // 本意是排除node_modules
  loader: 'css-loader'
}

结果把自己的/styles目录也排除了!应该写成:

exclude: /node_modules/

4. 核心区别总结

特性 include exclude
中文意思 包含 排除
适用场景 明确知道要哪些 明确知道不要哪些
安全性 更安全(白名单) 风险更高(可能漏网)
性能影响 范围小性能好 范围大时性能差
典型应用 路由守卫、loader处理范围 跳过不需要处理的资源

5. 黄金选择法则

  1. 优先用include:当你知道确切需要什么时(更安全)
  2. 谨慎用exclude:当你知道确切不需要什么时
  3. 不要混合用:容易导致逻辑混乱(见过有人同时写include和exclude结果相互抵消)

6. 面试常考题目

面试官:"你们项目为什么用exclude而不是include?"

我的回答:
"我们只在处理第三方库时用exclude跳过node_modules,其他场景都用include精确控制范围,这样既能保证安全又避免性能浪费。"

7. 趣味记忆法

  • include → "in"(在里面)→ 白名单
  • exclude → "ex"(前任)→ 黑名单

最后送大家一句话

"include是圈地养羊,exclude是篱笆防狼" —— 这是我在团队内部分享时说的,现在送给你们。

⭐  写在最后

请大家不吝赐教,在下方评论或者私信我,十分感谢🙏🙏🙏.

✅ 认为我某个部分的设计过于繁琐,有更加简单或者更高逼格的封装方式

✅ 认为我部分代码过于老旧,可以提供新的API或最新语法

✅ 对于文章中部分内容不理解

✅ 解答我文章中一些疑问

✅ 认为某些交互,功能需要优化,发现BUG

✅ 想要添加新功能,对于整体的设计,外观有更好的建议

✅ 一起探讨技术加qq交流群:906392632

最后感谢各位的耐心观看,既然都到这了,点个 👍赞再走吧!

Vue的响应式魔法:从惊艳到看透,6年老司机带你揭秘

大家好,我是小杨,一个写了6年前端的"魔法破解师"。今天咱们来聊聊Vue最核心的魔法——响应式系统。看完这篇,你会从"哇好神奇"变成"哦原来如此"!

1. 先看个魔法现场

data() {
  return {
    message: '你好'
  }
}
// 修改数据
this.message = '新消息' // 页面自动更新!

这魔法怎么实现的?咱们一层层扒开看!

2. 核心三板斧

Vue的响应式靠这三个家伙:

  • Observer(侦察兵):负责数据劫持
  • Dep(调度中心):管理依赖关系
  • Watcher(跑腿小哥):执行更新

3. 手撕源码级实现

① 数据劫持(Object.defineProperty)

function defineReactive(obj, key) {
  const dep = new Dep() // 每个属性配个调度中心
  let value = obj[key]
  
  Object.defineProperty(obj, key, {
    get() {
      if (Dep.target) { // 如果有跑腿小哥在待命
        dep.depend() // 登记依赖关系
      }
      return value
    },
    set(newVal) {
      value = newVal
      dep.notify() // 通知所有跑腿小哥
    }
  })
}

② 依赖收集(Dep类)

class Dep {
  constructor() {
    this.subs = [] // 存所有跑腿小哥
  }
  
  depend() {
    if (Dep.target) {
      this.subs.push(Dep.target)
    }
  }
  
  notify() {
    this.subs.forEach(sub => sub.update())
  }
}
Dep.target = null // 全局标记位

③ 更新触发(Watcher类)

class Watcher {
  constructor(vm, exp, cb) {
    this.cb = cb
    Dep.target = this // 立个flag
    vm._data[exp] // 触发getter,完成依赖收集
    Dep.target = null
  }
  
  update() {
    this.cb() // 执行更新
  }
}

4. 我踩过的三个大坑

坑① 对象新增属性不响应

this.user.age = 25 // 不触发更新!

解法this.$set(this.user, 'age', 25)

坑② 数组变异方法

this.items[0] = '新值' // 不触发!
this.items.length = 0 // 也不触发!

解法:重写数组方法(push/pop等)

坑③ 性能问题
深层嵌套对象劫持会消耗较大内存

5. Vue 3的超级升级(Proxy)

Vue 3改用Proxy,解决了Vue 2的痛点:

const data = new Proxy({ message: '你好' }, {
  get(target, key) {
    track(target, key) // 依赖收集
    return target[key]
  },
  set(target, key, value) {
    target[key] = value
    trigger(target, key) // 触发更新
    return true
  }
})

优势

  • 直接监听新增/删除属性
  • 更好的性能
  • 原生支持数组

6. 响应式的三大短板

  1. 初始化性能开销:递归劫持大对象较慢
  2. 内存占用:每个属性都要维护Dep实例
  3. 无法劫持ES6+新数据结构(Map/Set等)

7. 实战中的骚操作

我在低代码平台这样用:

// 动态添加响应式属性
function addReactiveProp(obj, key) {
  let value = obj[key]
  Object.defineProperty(obj, key, {
    get() { return value },
    set(newVal) {
      value = newVal
      publishChange(key) // 自定义发布逻辑
    }
  })
}

8. 写给新人的建议

  1. 理解原理比会用API更重要

  2. 遇到"数据变了视图不更新"先检查:

    • 是否在data中声明
    • 是否使用了非响应式API
  3. 复杂场景考虑用Vuex/Pinia

最后说句大实话

"Vue的响应式就像自动挡汽车,开起来爽但爆胎时得知道怎么换备胎" —— 这是我在团队内部分享时说的,现在送给你们。

⭐  写在最后

请大家不吝赐教,在下方评论或者私信我,十分感谢🙏🙏🙏.

✅ 认为我某个部分的设计过于繁琐,有更加简单或者更高逼格的封装方式

✅ 认为我部分代码过于老旧,可以提供新的API或最新语法

✅ 对于文章中部分内容不理解

✅ 解答我文章中一些疑问

✅ 认为某些交互,功能需要优化,发现BUG

✅ 想要添加新功能,对于整体的设计,外观有更好的建议

✅ 一起探讨技术加qq交流群:906392632

最后感谢各位的耐心观看,既然都到这了,点个 👍赞再走吧!

不用Vue,手搓一个数据双向绑定?教你用原生JS造轮子!

大家好,我是小杨,一个写了6年前端的老码农。今天咱们不聊Vue,来点刺激的——用原生JS实现Vue的数据双向绑定!看完这篇,你会恍然大悟:"原来Vue的黑魔法这么简单?"

1. 先看Vue的双向绑定多香

<!-- Vue版 -->
<input v-model="message">
<p>{{ message }}</p>

数据一变,视图自动更新,舒服吧?那原生JS咋实现呢?

2. 核心原理拆解

双向绑定其实就是:

  1. 数据变 → 视图变(数据劫持)
  2. 视图变 → 数据变(事件监听)

3. 手把手实现

第一步:数据劫持(Object.defineProperty)

const data = {
  message: '我是初始值'
}

// 劫持数据
function observe(obj) {
  Object.keys(obj).forEach(key => {
    let value = obj[key]
    Object.defineProperty(obj, key, {
      get() {
        console.log(`读取了${key}`)
        return value
      },
      set(newVal) {
        console.log(`${key}被修改为${newVal}`)
        value = newVal
        updateView() // 数据变时更新视图
      }
    })
  })
}

observe(data)

第二步:更新视图

function updateView() {
  document.getElementById('text').innerText = data.message
}

第三步:监听输入(事件绑定)

<input id="input" type="text">
<p id="text"></p>

<script>
document.getElementById('input').addEventListener('input', (e) => {
  data.message = e.target.value // 视图变时修改数据
})
</script>

4. 效果演示

现在试试:

  1. 在控制台修改 data.message = "新值" → 页面自动更新
  2. 在输入框打字 → data.message 同步变化

这不就是简易版v-model吗!

5. 我踩过的坑

第一次实现时我忘了处理嵌套对象:

const data = {
  user: {
    name: '小杨' // 这个子对象没被劫持!
  }
}

解决方案:递归劫持

function observe(obj) {
  if (typeof obj !== 'object') return
  
  Object.keys(obj).forEach(key => {
    let value = obj[key]
    observe(value) // 递归劫持
    // ...原来的defineProperty逻辑
  })
}

6. 进阶版:用Proxy实现(ES6)

更优雅的现代写法:

const data = new Proxy({ message: '你好' }, {
  set(target, key, value) {
    target[key] = value
    updateView()
    return true
  }
})

// 使用方式完全一样
data.message = '新消息' // 自动触发更新

7. 和Vue的差别在哪?

我们实现的简易版缺少:

  • 虚拟DOM优化
  • 依赖收集(Dep/Watcher)
  • 批量异步更新
  • 数组特殊处理

但核心思想一模一样!

8. 实际应用场景

我曾在老项目中用这个思路:

  • 实现表单联动(A输入框变,B选择框选项变)
  • 低代码平台的数据绑定
  • 简单的状态管理

最后送大家两句话:

  1. "理解原理最好的方式就是自己造轮子"
  2. "框架用着爽,但别忘记原生JS才是基本功"

⭐  写在最后

请大家不吝赐教,在下方评论或者私信我,十分感谢🙏🙏🙏.

✅ 认为我某个部分的设计过于繁琐,有更加简单或者更高逼格的封装方式

✅ 认为我部分代码过于老旧,可以提供新的API或最新语法

✅ 对于文章中部分内容不理解

✅ 解答我文章中一些疑问

✅ 认为某些交互,功能需要优化,发现BUG

✅ 想要添加新功能,对于整体的设计,外观有更好的建议

✅ 一起探讨技术加qq交流群:906392632

最后感谢各位的耐心观看,既然都到这了,点个 👍赞再走吧!

退出登录后头像还在?这个缓存问题坑过多少前端!

大家好,我是小杨,一个干了6年的前端老司机。今天要聊一个看似简单却经常被忽略的问题——为什么用户退出登录后,头像还显示在页面上?

这个问题我遇到过不止一次,甚至有一次差点被测试同学当成严重BUG提上来。其实背后的原因很简单,但解决起来有几个关键点需要注意。


1. 为什么退出登录后头像还在?

通常,头像不会自动消失,主要有以下几个原因:

① 缓存没清理干净

  • 浏览器缓存:图片可能被浏览器缓存了,即使退出登录,浏览器仍然显示旧的头像。
  • 前端状态没重置:Vue/React 的全局状态(如 Vuex、Redux)可能还保留着用户信息。

② 头像URL没更新

很多网站的头像是通过URL加载的,比如:

<img src="https://example.com/avatars/我的头像.jpg" />

如果退出登录后,前端没强制刷新页面或更新URL,浏览器可能仍然显示缓存中的旧图片。

③ 后端会话失效,但静态资源可访问

即使退出登录,头像图片如果放在公开可访问的路径下(如 /public/avatars/),浏览器仍然能加载到。


2. 怎么解决?5种常见方案

✅ 方案1:强制刷新页面(简单粗暴)

退出登录后,直接 window.location.reload(),让浏览器重新加载所有资源。

logout() {
  clearUserToken(); // 清除Token
  window.location.reload(); // 强制刷新
}

缺点:体验不好,页面会闪烁。

✅ 方案2:给头像URL加时间戳(推荐)

在头像URL后面加一个随机参数,让浏览器认为是新图片:

<img :src="`/avatars/${user.avatar}?t=${Date.now()}`" />

或者用 Vue 的 v-if 控制显示:

<img v-if="isLoggedIn" :src="user.avatar" />

✅ 方案3:清除前端缓存状态

如果用了 Vuex/Pinia,退出时一定要清空用户数据:

// store/user.js
actions: {
  logout() {
    this.user = null;
    localStorage.removeItem('token');
  }
}

✅ 方案4:后端返回默认头像(保险做法)

如果用户未登录,后端可以返回一个默认头像URL,而不是让前端处理缓存问题。

✅ 方案5:Service Worker 缓存控制(高级玩法)

如果你用了 PWA,可以通过 Service Worker 动态控制缓存策略:

// service-worker.js
self.addEventListener('fetch', (event) => {
  if (event.request.url.includes('avatar')) {
    event.respondWith(
      fetch(event.request, { cache: 'no-store' }) // 不缓存头像
    );
  }
});

3. 我踩过的坑:本地开发没问题,上线出BUG

有一次,我在本地测试退出登录功能,头像正常消失。但上线后,用户反馈退出后头像还在!

原因

  • 本地开发时,浏览器没缓存图片。
  • 生产环境用了 CDN,图片被缓存了,导致退出后仍然显示旧头像。

解决方案
在头像URL后面加版本号,比如:

<img :src="`/avatars/${user.avatar}?v=${user.avatarVersion}`" />

每次用户更新头像,后端都更新 avatarVersion,这样浏览器就会重新加载。


4. 终极解决方案:综合策略

最佳实践是 前端 + 后端 一起处理:

  1. 前端:退出时清空状态,加随机参数避免缓存。
  2. 后端:返回正确的 HTTP 缓存头(如 Cache-Control: no-store)。

5. 总结

  • 问题根源:浏览器缓存 + 前端状态没清理干净。

  • 解决方案

    • 加随机参数(?t=时间戳
    • 清空 Vuex/Redux 状态
    • 后端控制缓存策略
  • 高级方案:Service Worker 动态管理缓存

如果你也遇到过这个问题,欢迎在评论区分享你的解决方案! 🚀

⭐  写在最后

请大家不吝赐教,在下方评论或者私信我,十分感谢🙏🙏🙏.

✅ 认为我某个部分的设计过于繁琐,有更加简单或者更高逼格的封装方式

✅ 认为我部分代码过于老旧,可以提供新的API或最新语法

✅ 对于文章中部分内容不理解

✅ 解答我文章中一些疑问

✅ 认为某些交互,功能需要优化,发现BUG

✅ 想要添加新功能,对于整体的设计,外观有更好的建议

✅ 一起探讨技术加qq交流群:906392632

最后感谢各位的耐心观看,既然都到这了,点个 👍赞再走吧!

Vue的'读心术':它怎么知道数据偷偷变了?

大家好,我是小杨。做了6年前端,经常被新手问:"Vue怎么知道我修改了数据?"今天就来揭秘这个"读心术"!

1. 先看个神奇现象

data() {
  return {
    message: '你好'
  }
}

当我在代码中修改this.message = '新消息'时,视图自动更新了!这背后发生了什么?

2. 核心原理:数据劫持

Vue其实是个"老六",它偷偷做了三件事:

  1. 监听对象属性(Object.defineProperty)
  2. 建立依赖收集(Dep)
  3. 通知视图更新(Watcher)

3. 手写一个极简版

我们来模拟Vue的实现:

class 简易Vue {
  constructor(options) {
    this._data = options.data
    this.劫持数据(this._data)
  }
  
  劫持数据(obj) {
    Object.keys(obj).forEach(key => {
      let value = obj[key]
      Object.defineProperty(obj, key, {
        get() {
          console.log(`${key}被读取了`)
          return value
        },
        set(newVal) {
          console.log(`${key}${value}变成了${newVal}`)
          value = newVal
          // 这里应该通知视图更新
        }
      })
    })
  }
}

// 使用
const app = new 简易Vue({
  data: { message: '我是初始值' }
})
app._data.message = '我是新值' // 控制台会打印变化!

4. 我遇到的真实案例

曾经有个bug让我排查到凌晨3点:

data() {
  return {
    user: { name: '小杨' }
  }
}

// 错误写法!
this.user.age = 25 // 视图不会更新!

原因:Vue无法检测新增的属性!必须用this.$set(this.user, 'age', 25)

5. 数组的特殊处理

Vue对数组方法做了hack:

// 这些能触发更新
this.items.push('新项目')
this.items.splice(0, 1)

// 这些不行!
this.items[0] = '修改项' // 要用Vue.set
this.items.length = 0 // 不会触发

6. Vue 3的升级版:Proxy

Vue 3改用Proxy实现,解决了Vue 2的限制:

const data = new Proxy({ message: '你好' }, {
  set(target, key, value) {
    console.log(`检测到${key}变化`)
    target[key] = value
    return true
  }
})

data.message = '再见' // 自动触发set

7. 性能优化小技巧

  1. 冻结不需要响应的数据Object.freeze
  2. 扁平化数据结构:嵌套太深影响性能
  3. 避免在模板中使用复杂表达式

8. 调试技巧

想知道谁修改了数据?在组件中添加:

watch: {
  message(newVal, oldVal) {
    console.log(`[小杨的调试] message从${oldVal}变成了${newVal}`)
  }
}

最后说句掏心窝的

理解响应式原理后,再看Vue就像开了透视挂。下次遇到"视图不更新"的问题,你就能快速定位了!

⭐  写在最后

请大家不吝赐教,在下方评论或者私信我,十分感谢🙏🙏🙏.

✅ 认为我某个部分的设计过于繁琐,有更加简单或者更高逼格的封装方式

✅ 认为我部分代码过于老旧,可以提供新的API或最新语法

✅ 对于文章中部分内容不理解

✅ 解答我文章中一些疑问

✅ 认为某些交互,功能需要优化,发现BUG

✅ 想要添加新功能,对于整体的设计,外观有更好的建议

✅ 一起探讨技术加qq交流群:906392632

最后感谢各位的耐心观看,既然都到这了,点个 👍赞再走吧!

手把手教你造一个自己的v-model:原来双向绑定这么简单!

大家好,我是小杨,一个写了6年前端的老码农。今天想带大家揭开Vue里v-model的神秘面纱,我们自己动手实现一个简易版!

记得刚学Vue时,我觉得v-model简直是黑魔法——输入框的值怎么就自动同步到数据了呢?直到有一天我看了源码,才发现...

1. v-model的本质是什么?

一句话:语法糖!
它其实就是value属性 + @input事件的快捷写法。比如:

<input v-model="message">

等价于:

<input 
  :value="message"
  @input="message = $event.target.value"
>

2. 自己实现一个简易v-model

让我们造个轮子叫my-model

<template>
  <input 
    :value="value"
    @input="$emit('input', $event.target.value)"
  >
</template>

<script>
export default {
  props: ['value']
}
</script>

使用时:

<my-model v-model="message"></my-model>

效果:  和官方v-model一模一样!不信你试试。

3. 我踩过的坑

有次我自作聪明加了额外功能:

@input="handleInput($event.target.value)"

然后在methods里:

handleInput(val) {
  this.$emit('input', val + '后缀') // 自动加后缀
}

结果用户每输入一个字符就追加后缀,直接炸了😂。所以直接emit原始值最安全!

4. 进阶玩法:自定义组件的v-model

Vue 2.x默认使用value属性和input事件,但我们可以改!

model: {
  prop: '我喜欢的名字',  // 改用其他属性名
  event: 'change'      // 改用其他事件名
}

这样就能:

<custom-input 
  v-model="message"
  我喜欢的名字="初始值"
  @change="处理函数"
></custom-input>

5. Vue 3的小变化

Vue 3中更灵活了:

  • 默认属性名改为modelValue
  • 默认事件名改为update:modelValue
  • 支持多个v-model绑定
<MyComponent v-model:title="title" v-model:content="content" />

6. 活学活用案例

我做过一个颜色选择器组件:

<color-picker v-model="themeColor" />

内部实现:

// 当用户选颜色时
this.$emit('input', newColor)

这样父组件完全不用写监听逻辑,干净又卫生!

7. 为什么理解这个很重要?

  1. 面试常考题(我当面试官必问)
  2. 自定义表单组件必备技能
  3. 避免滥用v-model(有些场景应该用.sync)

最后送大家一句话:

"理解v-model,就是理解Vue双向绑定的第一课" —— 这是当年我的导师说的,现在送给你们。

⭐  写在最后

请大家不吝赐教,在下方评论或者私信我,十分感谢🙏🙏🙏.

✅ 认为我某个部分的设计过于繁琐,有更加简单或者更高逼格的封装方式

✅ 认为我部分代码过于老旧,可以提供新的API或最新语法

✅ 对于文章中部分内容不理解

✅ 解答我文章中一些疑问

✅ 认为某些交互,功能需要优化,发现BUG

✅ 想要添加新功能,对于整体的设计,外观有更好的建议

✅ 一起探讨技术加qq交流群:906392632

最后感谢各位的耐心观看,既然都到这了,点个 👍赞再走吧!

v-for中key值的作用:为什么我总被要求加这个'没用的'属性?

大家好,我是小杨,一个干了6年的前端老油条。今天想和大家聊聊Vue中一个看似简单却经常被问起的问题——v-for里的key值到底有什么用。

记得我刚学Vue那会儿,每次用v-for都会收到ESLint的红色警告:"Elements in iteration expect to have 'v-bind:key' directives"。当时的我总在想:"不加不也能用吗?这玩意儿到底有啥用?"

1. key值是什么?

简单说,key就是给每个循环项一个"身份证号"。比如我们渲染一个列表:

<ul>
  <li v-for="item in items" :key="item.id">
    {{ 我 }}喜欢{{ item.name }}
  </li>
</ul>

2. 为什么需要key?

Vue需要key来高效地更新DOM。没有key时,当列表顺序变化,Vue会怎么做?它会直接就地更新元素,而不是移动它们。

举个我踩过的坑:

// 初始数据
items: [
  { id: 1, name: '苹果' },
  { id: 2, name: '香蕉' }
]

// 后来数据变成了
items: [
  { id: 2, name: '香蕉' },
  { id: 1, name: '苹果' }
]

没有key时,Vue不会交换这两个li的位置,而是直接更新内容。这会导致:

  1. 性能浪费(不必要的DOM更新)
  2. 可能的状态问题(比如输入框内容错乱)

3. key的正确打开方式

✅ 正确做法:

<li v-for="item in items" :key="item.id">

❌ 错误做法:

<li v-for="item in items" :key="index">

(用index当key和没加差不多,特别是列表会变化时)

4. 我总结的key使用原则

  1. 唯一性:key应该在当前列表中唯一
  2. 稳定性:key不应该随时间改变(别用随机数!)
  3. 可预测性:相同内容应该生成相同key

5. 实际工作中的经验

有次我做了一个复杂的列表组件,每个项都有内部状态。最初偷懒用了index当key,结果用户排序时各种bug。后来老老实实改用item.id,问题迎刃而解。

6. 什么时候可以不加key?

理论上说,纯静态列表(不会排序、过滤、修改)可以不加。但我的建议是:永远加上key!这就像系安全带,平时觉得麻烦,关键时刻能救命。

最后

key值看似是个小细节,却体现了Vue的响应式原理。理解它不仅能避免bug,还能写出更高性能的代码。希望我的经验对你有帮助!

小贴士:如果你也在纠结key的问题,记住这句话——"给Vue一个靠谱的身份证,它还你一个稳定的列表渲染"。

⭐  写在最后

请大家不吝赐教,在下方评论或者私信我,十分感谢🙏🙏🙏.

✅ 认为我某个部分的设计过于繁琐,有更加简单或者更高逼格的封装方式

✅ 认为我部分代码过于老旧,可以提供新的API或最新语法

✅ 对于文章中部分内容不理解

✅ 解答我文章中一些疑问

✅ 认为某些交互,功能需要优化,发现BUG

✅ 想要添加新功能,对于整体的设计,外观有更好的建议

✅ 一起探讨技术加qq交流群:906392632

最后感谢各位的耐心观看,既然都到这了,点个 👍赞再走吧!

Vue计算属性:为什么我的代码突然变优雅了?

大家好,我是小杨,一个写了6年前端的老油条。今天想和大家聊聊Vue中一个看似简单但超级实用的功能——计算属性。记得我刚接触Vue时,总觉得data和methods已经够用了,直到发现了计算属性这个"神器",我的代码才真正开始变得优雅起来。

一、计算属性是什么?

简单来说,计算属性就是基于现有数据计算出来的新数据。它就像一个智能的中间人,帮你处理data中的数据,给你想要的结果。

举个🌰,假设我要显示用户的全名:

data() {
  return {
    firstName: '小',
    lastName: '杨'
  }
}

没有计算属性时,我可能会这样写:

<p>{{ firstName + ' ' + lastName }}</p>

或者用方法:

methods: {
  fullName() {
    return this.firstName + ' ' + this.lastName
  }
}

但有了计算属性,事情就变得简单多了:

computed: {
  fullName() {
    return this.firstName + ' ' + this.lastName
  }
}

看起来和方法差不多?别急,它的妙处还在后面呢!

二、计算属性的三大超能力

1. 自动缓存 - 懒人的福音

计算属性最厉害的特点就是缓存。只要依赖的数据不改变,多次访问计算属性会立即返回之前缓存的结果,而不会重新计算。

比如我有一个复杂的计算:

computed: {
  complicatedCalculation() {
    console.log('重新计算了!')
    return this.someData * 100 / Math.PI + 1000
  }
}

即使我在模板里用十次:

<p>{{ complicatedCalculation }}</p>
<p>{{ complicatedCalculation }}</p>
<p>{{ complicatedCalculation }}</p>

控制台只会输出一次"重新计算了!"。如果是方法,每次都会重新执行。

2. 响应式依赖追踪 - 智能的管家

计算属性会自动追踪它依赖的响应式数据。只有当依赖变化时,它才会重新计算。

computed: {
  userInfo() {
    return {
      name: this.user.name,
      age: this.user.age,
      // 只要user.address没被用到,address变化不会触发重新计算
    }
  }
}

3. 可读可写 - 灵活的双向门

计算属性默认只有getter,但也可以提供setter:

computed: {
  fullName: {
    get() {
      return this.firstName + ' ' + this.lastName
    },
    set(newValue) {
      const names = newValue.split(' ')
      this.firstName = names[0]
      this.lastName = names[1] || ''
    }
  }
}

现在你可以这样用:

// 读取
console.log(this.fullName)

// 设置
this.fullName = '大 杨'

三、什么时候该用计算属性?

根据我的经验,以下场景特别适合使用计算属性:

1. 复杂的数据转换

比如从后端拿到一组数据,需要加工后再显示:

computed: {
  filteredProducts() {
    return this.products.filter(p => p.price > 100)
                      .sort((a,b) => b.price - a.price)
                      .slice(0, 5)
  }
}

2. 表单验证

computed: {
  emailError() {
    if (!this.email) return '邮箱不能为空'
    if (!/.+@.+..+/.test(this.email)) return '邮箱格式不正确'
    return ''
  },
  passwordError() {
    if (!this.password) return '密码不能为空'
    if (this.password.length < 6) return '密码太短'
    return ''
  },
  isValid() {
    return !this.emailError && !this.passwordError
  }
}

3. 组件props的派生状态

props: ['size'],
computed: {
  normalizedSize() {
    return this.size.trim().toLowerCase()
  }
}

四、计算属性 vs 方法 vs 侦听器

很多新手会困惑:什么时候用计算属性,什么时候用方法,什么时候用watch?

计算属性 vs 方法

  • 计算属性:适合需要缓存的结果,基于响应式依赖
  • 方法:适合不需要缓存,或者需要参数的情况
// 计算属性 - 无参数,自动缓存
computed: {
  currentDate() {
    return new Date().toLocaleDateString()
  }
}

// 方法 - 可以有参数,每次调用都执行
methods: {
  formatDate(date) {
    return new Date(date).toLocaleDateString()
  }
}

计算属性 vs 侦听器

  • 计算属性:声明式的,你告诉Vue你想要什么
  • 侦听器:命令式的,你告诉Vue当某些数据变化时要做什么
// 计算属性 - 更简洁
computed: {
  fullName() {
    return this.firstName + ' ' + this.lastName
  }
}

// 侦听器 - 更灵活
watch: {
  firstName(newVal) {
    this.fullName = newVal + ' ' + this.lastName
  },
  lastName(newVal) {
    this.fullName = this.firstName + ' ' + newVal
  }
}

五、计算属性的高级用法

1. 结合v-model使用

computed: {
  searchQuery: {
    get() {
      return this.$store.state.searchQuery
    },
    set(value) {
      this.$store.commit('updateSearchQuery', value)
    }
  }
}

然后在模板中:

<input v-model="searchQuery">

2. 动态计算属性

有时候你可能需要动态创建计算属性:

computed: {
  dynamicComputed() {
    return () => {
      // 根据某些条件返回不同的计算逻辑
      if (this.mode === 'simple') {
        return this.data.length
      } else {
        return this.data.reduce((sum, item) => sum + item.value, 0)
      }
    }
  }
}

3. 组合式API中的计算属性

在Vue3的组合式API中,计算属性用起来也很简单:

import { computed } from 'vue'

setup() {
  const count = ref(0)
  
  const doubleCount = computed(() => count.value * 2)
  
  return {
    count,
    doubleCount
  }
}

六、性能优化小技巧

  1. 避免在计算属性中做复杂操作:计算属性会在依赖变化时重新计算,复杂的操作会影响性能
  2. 不要修改依赖数据:计算属性应该是纯函数,不要在里面修改依赖的数据
  3. 合理拆分计算属性:一个计算属性只做一件事,可以提高可读性和维护性
  4. 避免长依赖链:计算属性依赖其他计算属性时,链条太长会影响性能

七、常见坑点

  1. 异步操作:计算属性不能包含异步操作,这时候应该用方法或者watch
  2. 副作用:计算属性不应该有副作用(如修改DOM、发起请求等)
  3. 依赖未声明:如果计算属性依赖的数据没有在data中声明,Vue无法追踪变化
// 错误示范
computed: {
  badComputed() {
    return window.innerWidth // window不是响应式的!
  }
}

八、我的实战经验

在做一个电商项目时,我遇到过这样一个需求:需要根据用户选择的筛选条件动态显示商品列表。最初我用watch来实现,代码变得又长又难维护。后来改用计算属性,代码量减少了60%!

重构前:

data() {
  return {
    products: [],
    filteredProducts: [],
    category: '',
    priceRange: [0, 1000],
    sortBy: 'price'
  }
},
watch: {
  category() {
    this.filterProducts()
  },
  priceRange() {
    this.filterProducts()
  },
  sortBy() {
    this.filterProducts()
  }
},
methods: {
  filterProducts() {
    // 一大段过滤和排序逻辑
  }
}

重构后:

computed: {
  filteredProducts() {
    let result = this.products
    
    // 按类别过滤
    if (this.category) {
      result = result.filter(p => p.category === this.category)
    }
    
    // 按价格范围过滤
    result = result.filter(p => 
      p.price >= this.priceRange[0] && 
      p.price <= this.priceRange[1]
    )
    
    // 排序
    if (this.sortBy === 'price') {
      result = [...result].sort((a, b) => a.price - b.price)
    } else if (this.sortBy === 'sales') {
      result = [...result].sort((a, b) => b.sales - a.sales)
    }
    
    return result
  }
}

代码不仅更简洁,而且性能也更好,因为计算属性会自动缓存结果,只有依赖变化时才会重新计算。

九、总结

计算属性是Vue中一个非常强大的特性,它能够:

  1. 让你的代码更简洁、更易读
  2. 自动缓存计算结果,提高性能
  3. 智能追踪依赖,只在需要时重新计算
  4. 可以读写结合,处理复杂逻辑

记住:当你需要基于现有数据派生新数据时,首先考虑计算属性。它能让你的Vue代码从"能用"升级到"优雅"的水平。

我是小杨,一个喜欢分享的前端开发者。如果这篇文章对你有帮助,别忘了点赞收藏。如果有任何问题,欢迎在评论区留言讨论!

⭐  写在最后

请大家不吝赐教,在下方评论或者私信我,十分感谢🙏🙏🙏.

✅ 认为我某个部分的设计过于繁琐,有更加简单或者更高逼格的封装方式

✅ 认为我部分代码过于老旧,可以提供新的API或最新语法

✅ 对于文章中部分内容不理解

✅ 解答我文章中一些疑问

✅ 认为某些交互,功能需要优化,发现BUG

✅ 想要添加新功能,对于整体的设计,外观有更好的建议

✅ 一起探讨技术加qq交流群:906392632

最后感谢各位的耐心观看,既然都到这了,点个 👍赞再走吧!

Vue路由钩子全攻略:让你的页面跳转更丝滑!

大家好,我是小杨,一个写了6年前端的老司机。今天咱们聊聊Vue路由里的钩子函数,这些钩子就像是路由的"关卡守卫",能在页面跳转前后做各种骚操作,比如权限拦截、数据预加载、页面过渡动画等等。

1. 路由钩子是什么?能干啥?

简单来说,路由钩子就是Vue Router在路由切换时提供的回调函数,可以在不同阶段拦截或处理路由跳转。比如:

  • 登录拦截:没登录?滚回登录页!
  • 数据预加载:进页面之前先把数据请求好
  • 页面权限控制:你没权限?抱歉,此路不通
  • 滚动行为控制:跳转后自动回到顶部

2. 路由钩子分类

Vue Router的钩子主要分三类:

  1. 全局钩子(整个路由生效)
  2. 路由独享钩子(只对某个路由生效)
  3. 组件内钩子(在组件内部使用)

(1)全局钩子:整个路由的守门人

① beforeEach(路由跳转前)

router.beforeEach((to, from, next) => {
  if (to.meta.requiresAuth && !我.isLoggedIn) {
    next('/login') // 没登录?去登录页!
  } else {
    next() // 放行
  }
})

适用场景:全局权限控制、登录拦截

② afterEach(路由跳转后)

router.afterEach((to, from) => {
  sendAnalytics(to.path) // 统计页面访问
})

适用场景:埋点统计、页面访问日志

(2)路由独享钩子:单个路由的专属逻辑

const routes = [
  {
    path: '/dashboard',
    component: Dashboard,
    beforeEnter: (to, from, next) => {
      if (!我.hasPermission('admin')) {
        next('/403') // 没权限?去403页面!
      } else {
        next()
      }
    }
  }
]

适用场景:特定路由的权限控制

(3)组件内钩子:组件自己的路由逻辑

在组件里可以直接用这些钩子:

  • beforeRouteEnter(进入组件前)
  • beforeRouteUpdate(路由参数变化但组件复用时)
  • beforeRouteLeave(离开组件前)

① beforeRouteEnter(进入组件前)

export default {
  beforeRouteEnter(to, from, next) {
    // 这里还不能用 `this`,因为组件还没创建!
    next(vm => {
      console.log(vm.myData) // 现在可以访问组件实例了
    })
  }
}

适用场景:进入页面前的数据预加载

② beforeRouteUpdate(路由参数变化时)

export default {
  beforeRouteUpdate(to, from, next) {
    if (to.params.id !== from.params.id) {
      this.fetchData(to.params.id) // 重新获取数据
    }
    next()
  }
}

适用场景:动态路由参数变化时刷新数据

③ beforeRouteLeave(离开页面时)

export default {
  beforeRouteLeave(to, from, next) {
    if (this.unsavedChanges) {
      if (confirm('有未保存的内容,确定离开吗?')) {
        next()
      } else {
        next(false) // 取消导航
      }
    } else {
      next()
    }
  }
}

适用场景:防止用户误操作离开页面(比如填了一半的表单)

3. 实际开发中的经典场景

场景1:登录拦截

router.beforeEach((to, from, next) => {
  if (to.meta.requiresAuth && !localStorage.getItem('token')) {
    next('/login?redirect=' + to.fullPath) // 记录跳转目标,登录后自动回来
  } else {
    next()
  }
})

场景2:动态修改页面标题

router.afterEach((to) => {
  document.title = to.meta.title || '默认标题'
})

场景3:滚动行为控制

const router = new VueRouter({
  scrollBehavior(to, from, savedPosition) {
    if (savedPosition) {
      return savedPosition // 返回之前的位置(比如浏览器后退时)
    } else {
      return { x: 0, y: 0 } // 默认滚动到顶部
    }
  }
})

4. 总结

  • 全局钩子beforeEachafterEach(适合全局逻辑)
  • 路由独享钩子beforeEnter(适合单个路由的逻辑)
  • 组件内钩子beforeRouteEnterbeforeRouteUpdatebeforeRouteLeave(适合组件内部逻辑)

合理使用这些钩子,能让你的路由跳转更智能、更安全!

⭐  写在最后

请大家不吝赐教,在下方评论或者私信我,十分感谢🙏🙏🙏.

✅ 认为我某个部分的设计过于繁琐,有更加简单或者更高逼格的封装方式

✅ 认为我部分代码过于老旧,可以提供新的API或最新语法

✅ 对于文章中部分内容不理解

✅ 解答我文章中一些疑问

✅ 认为某些交互,功能需要优化,发现BUG

✅ 想要添加新功能,对于整体的设计,外观有更好的建议

✅ 一起探讨技术加qq交流群:906392632

最后感谢各位的耐心观看,既然都到这了,点个 👍赞再走吧!

Vuex数据突然消失?六招教你轻松找回来!

大家好,我是小杨,一个经历过无数次Vuex数据"失踪案"的前端老司机。今天要和大家分享的是我在项目中遇到的Vuex数据丢失问题以及解决方案。记得有一次项目上线后,用户反馈登录状态总是莫名其妙丢失,我排查了整整两天才发现问题所在...

一、页面刷新导致数据丢失

问题现象:刷新页面后,Vuex中的数据全部归零。

原因分析:Vuex的状态是存储在内存中的,刷新页面相当于重置了JavaScript运行环境。

解决方案

1. 使用vuex-persistedstate插件

// store/index.js
import createPersistedState from 'vuex-persistedstate'

export default new Vuex.Store({
  // ...
  plugins: [createPersistedState()]
})

这个插件会自动将state保存到localStorage,刷新后恢复数据。

2. 手动持久化关键数据

// 在mutation中保存数据
mutations: {
  SET_USER(state, user) {
    state.user = user
    localStorage.setItem('user', JSON.stringify(user))
  }
}

// 在store初始化时恢复数据
const user = JSON.parse(localStorage.getItem('user'))
const store = new Vuex.Store({
  state: {
    user: user || null
    // ...
  }
})

二、路由跳转导致数据丢失

问题现象:某些页面跳转后,部分Vuex数据不见了。

原因分析:可能是路由配置问题或组件生命周期导致的。

解决方案

1. 检查路由配置

const router = new VueRouter({
  mode: 'history',
  routes: [
    {
      path: '/user/:id',
      component: User,
      props: true,  // 保持组件实例复用
    }
  ]
})

2. 使用keep-alive缓存组件

<template>
  <keep-alive>
    <router-view v-if="$route.meta.keepAlive"></router-view>
  </keep-alive>
  <router-view v-if="!$route.meta.keepAlive"></router-view>
</template>

三、异步操作导致的数据不一致

问题现象:数据看似丢失,实际上是异步操作未完成导致的显示问题。

解决方案

1. 添加loading状态

state: {
  user: null,
  loading: false
},
actions: {
  async fetchUser({ commit }) {
    commit('SET_LOADING', true)
    try {
      const user = await api.getUser()
      commit('SET_USER', user)
    } finally {
      commit('SET_LOADING', false)
    }
  }
}

2. 使用v-if控制渲染时机

<template>
  <div v-if="!loading">
    <!-- 用户数据展示 -->
  </div>
  <div v-else>
    加载中...
  </div>
</template>

四、模块化导致的命名冲突

问题现象:模块中的数据似乎被重置或覆盖。

解决方案

1. 检查模块命名空间

const userModule = {
  namespaced: true,  // 关键!
  state: { /* ... */ },
  mutations: { /* ... */ }
}

2. 正确使用map辅助函数

computed: {
  ...mapState('user', ['info']),  // 指定命名空间
},
methods: {
  ...mapActions('user', ['fetchUser'])
}

五、开发环境热重载导致的问题

问题现象:开发时保存代码后,部分状态丢失。

解决方案

1. 配置webpack热重载

// store/index.js
export default new Vuex.Store({
  // ...
  strict: process.env.NODE_ENV !== 'production'
})

if (module.hot) {
  module.hot.accept(['./modules'], () => {
    store.hotUpdate({
      modules: {
        ...require('./modules').default
      }
    })
  })
}

六、浏览器隐私模式导致的问题

问题现象:在隐私模式下,数据无法持久化。

解决方案

1. 添加错误处理

function safeLocalStorageSet(key, value) {
  try {
    localStorage.setItem(key, JSON.stringify(value))
    return true
  } catch (e) {
    console.warn('本地存储失败:', e)
    return false
  }
}

2. 降级方案

// 使用sessionStorage作为fallback
function persistData(key, value) {
  if (!safeLocalStorageSet(key, value)) {
    sessionStorage.setItem(key, JSON.stringify(value))
  }
}

七、终极解决方案组合拳

在我的电商项目中,我是这样综合应用的:

// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import createPersistedState from 'vuex-persistedstate'

Vue.use(Vuex)

const store = new Vuex.Store({
  plugins: [
    createPersistedState({
      paths: ['user', 'cart'],  // 只持久化关键数据
      storage: {
        getItem: key => {
          // 我是小杨,这里添加安全读取逻辑
          try {
            return localStorage.getItem(key)
          } catch (e) {
            return sessionStorage.getItem(key)
          }
        },
        setItem: (key, value) => {
          try {
            localStorage.setItem(key, value)
          } catch (e) {
            sessionStorage.setItem(key, value)
          }
        },
        removeItem: key => {
          localStorage.removeItem(key)
          sessionStorage.removeItem(key)
        }
      }
    })
  ],
  modules: {
    // 模块化组织
  }
})

export default store

八、写在最后

Vuex数据丢失问题看似简单,但实际上可能有很多隐藏的原因。通过本文介绍的六种解决方案,你应该能够应对大多数场景:

  1. 页面刷新:使用持久化插件
  2. 路由跳转:检查路由配置和组件缓存
  3. 异步操作:添加loading状态
  4. 模块冲突:正确使用命名空间
  5. 热重载问题:配置webpack热更新
  6. 隐私模式:添加错误处理和降级方案

⭐  写在最后

请大家不吝赐教,在下方评论或者私信我,十分感谢🙏🙏🙏.

✅ 认为我某个部分的设计过于繁琐,有更加简单或者更高逼格的封装方式

✅ 认为我部分代码过于老旧,可以提供新的API或最新语法

✅ 对于文章中部分内容不理解

✅ 解答我文章中一些疑问

✅ 认为某些交互,功能需要优化,发现BUG

✅ 想要添加新功能,对于整体的设计,外观有更好的建议

✅ 一起探讨技术加qq交流群:906392632

最后感谢各位的耐心观看,既然都到这了,点个 👍赞再走吧!

Vue状态管理进阶:数据到底是怎么"跑"的?

大家好,我是小杨,一个摸爬滚打了6年的前端老司机。今天想和大家聊聊Vue项目中状态管理的数据流向问题。记得我第一次用Vuex时,被各种action、mutation搞得晕头转向,数据就像迷宫里的老鼠,完全不知道它从哪来,要往哪去。今天,我就来给大家画张清晰的"数据地图"!

一、为什么需要状态管理?

先讲个真实案例。去年我做了一个电商后台项目,开始没用状态管理,各个组件各自为政:

// 商品列表组件
data() {
  return {
    products: []
  }
},
created() {
  fetchProducts().then(res => {
    this.products = res.data
  })
}

// 购物车组件
data() {
  return {
    products: []
  }
},
created() {
  fetchProducts().then(res => {
    this.products = res.data.filter(p => p.inCart)
  })
}

发现问题了吗?同样的数据,重复请求,状态不同步,维护起来简直要命!这就是我们需要状态管理的原因——让数据有且只有一份真相来源

二、Vuex数据流向全景图

先来看张我手绘的核心流程图(文字版):

组件 → (dispatch) → Action → (commit) → Mutation → (mutate) → State → (render) → 组件

简单来说,数据流动是单向闭环的,就像单行道,不会出现"逆行"的情况。

三、分步拆解数据旅程

1. 组件触发Action

当组件中需要修改状态时,不能直接改,而是要通过dispatch触发一个action:

// 在组件中
this.$store.dispatch('addToCart', productId)

// 或者使用mapActions
methods: {
  ...mapActions(['addToCart'])
}

2. Action处理异步操作

Action像是业务逻辑的"调度中心",可以处理异步操作:

actions: {
  async addToCart({ commit }, productId) {
    try {
      // 我是小杨,这里模拟API调用
      const res = await api.addToCart(productId)
      commit('ADD_TO_CART', res.data)
    } catch (error) {
      commit('SET_ERROR', error.message)
    }
  }
}

3. Mutation修改状态

只有mutation能直接修改state,而且必须是同步的:

mutations: {
  ADD_TO_CART(state, product) {
    state.cart.push(product)
    state.totalPrice += product.price
  }
}

4. State变化触发更新

State变化后,所有依赖该状态的组件都会自动更新:

// 组件中获取状态
computed: {
  cart() {
    return this.$store.state.cart
  },
  // 或者使用mapState
  ...mapState(['cart', 'totalPrice'])
}

四、Pinia的数据流向(Vue3推荐)

Pinia是新一代Vue状态管理工具,更简单直观:

组件 → (直接调用) → Action → (直接修改) → State → 组件

对比Vuex:

  1. 没有mutation了,action可以直接修改state
  2. 更灵活的TypeScript支持
  3. 模块化开箱即用
// store/cart.js
export const useCartStore = defineStore('cart', {
  state: () => ({
    items: []
  }),
  actions: {
    async addToCart(productId) {
      const product = await api.getProduct(productId)
      this.items.push(product)
    }
  }
})

// 组件中使用
import { useCartStore } from '@/store/cart'
const cartStore = useCartStore()
cartStore.addToCart(123)

五、数据流动的黄金法则

根据我的踩坑经验,总结几条铁律:

  1. 组件不要直接修改state - 就像交通规则,破坏单向数据流迟早出bug
  2. 异步操作放action - mutation只做最简单的状态变更
  3. 保持state最小化 - 只存储必要数据,派生数据用getter
  4. 模块化设计 - 像整理衣柜一样组织你的store

六、实战:购物车数据流案例

让我们用Vuex实现一个完整的购物车流程:

// store/index.js
state: {
  cart: [],
  inventory: {}
},
getters: {
  cartTotal: state => {
    return state.cart.reduce((total, item) => total + item.price * item.quantity, 0)
  }
},
actions: {
  async checkout({ commit, state }) {
    // 我是小杨,这里处理结账逻辑
    const res = await api.checkout(state.cart)
    commit('CLEAR_CART')
    return res
  }
},
mutations: {
  ADD_ITEM(state, product) {
    const item = state.cart.find(i => i.id === product.id)
    item ? item.quantity++ : state.cart.push({ ...product, quantity: 1 })
  },
  CLEAR_CART(state) {
    state.cart = []
  }
}

组件中的使用:

// ProductItem.vue
methods: {
  addToCart() {
    this.$store.dispatch('addToCart', this.product)
  }
}

// ShoppingCart.vue
computed: {
  ...mapState(['cart']),
  ...mapGetters(['cartTotal'])
},
methods: {
  checkout() {
    this.$store.dispatch('checkout').then(() => {
      this.$router.push('/thank-you')
    })
  }
}

七、常见问题排查

1. 状态变了视图不更新?

  • 可能是直接修改了数组或对象,应该用Vue.set或展开运算符
  • 检查是否违反了单向数据流原则

2. 异步操作结果不一致?

  • 确保mutation是同步的
  • 考虑加loading状态和错误处理

3. 模块间如何共享状态?

  • 对于全局状态放在根store
  • 模块间通信可以通过rootState或派发其他模块的action

八、写在最后

理解数据流向就像掌握了交通规则,能让你的应用运行得更加顺畅。记住:

  1. Vuex是严格的单向数据流
  2. Pinia更灵活但也要遵循规范
  3. 良好的状态设计能大幅降低维护成本

⭐  写在最后

请大家不吝赐教,在下方评论或者私信我,十分感谢🙏🙏🙏.

✅ 认为我某个部分的设计过于繁琐,有更加简单或者更高逼格的封装方式

✅ 认为我部分代码过于老旧,可以提供新的API或最新语法

✅ 对于文章中部分内容不理解

✅ 解答我文章中一些疑问

✅ 认为某些交互,功能需要优化,发现BUG

✅ 想要添加新功能,对于整体的设计,外观有更好的建议

✅ 一起探讨技术加qq交流群:906392632

最后感谢各位的耐心观看,既然都到这了,点个 👍赞再走吧!

❌