2016年,Vue.js 2.0发布了,我也开始上手Vue.js 2.0。
说起来,最早接触Vue.js,是在2015年,那时候Vue.js还是1.0版本,比较小众,用的人不多。但是,它的简单易学、轻量高效,给我留下了很深的印象。
2016年,Vue.js 2.0发布了,做了很多改进,性能更好,功能更强,API也更完善。而且,这一年,Vue.js越来越流行,社区越来越活跃,很多公司开始用Vue.js做项目。
正好,我们有一个新项目,需要选一个前端框架,对比了React、Angular、Vue.js之后,我们选择了Vue.js 2.0,因为它简单易学,上手快,适合我们团队的情况。
于是,我开始上手Vue.js 2.0,把它用到了项目中。但是,在上手的过程中,我踩了很多坑,有些问题,甚至让我熬夜排查。
今天,就来聊聊Vue.js 2.0上手踩坑记,那些让我熬夜的问题。
一、Vue.js是什么
先简单介绍一下Vue.js是什么。
Vue.js(读音 /vjuː/,类似于 view)是一套用于构建用户界面的渐进式JavaScript框架。与其它大型框架不同的是,Vue被设计为可以自底向上逐层应用。Vue的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。
Vue.js的特点:
- 渐进式:可以逐步采用,不需要一开始就用全部功能
- 简单易学:API简单,文档清晰,上手快
- 轻量高效:核心库小,性能好,虚拟DOM优化
- 响应式:数据驱动视图,数据变化视图自动更新
- 组件化:组件化开发,代码复用,可维护性好
- 生态丰富:有Vuex(状态管理)、Vue Router(路由)、Vue CLI(脚手架)等官方工具
Vue.js 2.0相比1.0,做了很多改进:
- 性能更好,渲染速度提升了2-4倍
- 虚拟DOM重写,更高效
- 支持服务端渲染(SSR)
- 组件API更统一
- 支持JSX
- 更好的TypeScript支持
二、响应式原理
Vue.js最核心的特性就是响应式,数据变化,视图自动更新。但是,响应式也是最容易踩坑的地方。
1. 响应式原理
Vue.js 2.0的响应式,是通过Object.defineProperty实现的。在初始化的时候,Vue会遍历data对象的所有属性,用Object.defineProperty把它们转为getter/setter,在getter中收集依赖,在setter中触发更新。
当数据变化时,setter被触发,通知依赖(Watcher),Watcher触发重新渲染,更新视图。
这个原理,看起来简单,但是,有几个限制,也是容易踩坑的地方。
2. 无法检测对象属性的添加和删除
由于Object.defineProperty的限制,Vue无法检测对象属性的添加和删除。
比如:
data() {
return {
user: {
name: 'test'
}
};
},
methods: {
addAge() {
this.user.age = 18; // 这个属性不是响应式的,视图不会更新
}
}这里,给user对象添加了age属性,但是,这个属性不是响应式的,因为Vue在初始化的时候,只把user.name转为了getter/setter,age是后来添加的,没有被转为getter/setter,所以,数据变化,视图不会更新。
解决方法:
- 用Vue.set(object, key, value)或者this.$set(object, key, value)
- 或者,用Object.assign创建一个新对象,替换原对象
// 方法1:用Vue.set
this.$set(this.user, 'age', 18);
// 方法2:用Object.assign
this.user = Object.assign({}, this.user, { age: 18 });3. 无法检测数组索引的修改和长度修改
同样,Vue也无法检测数组索引的修改和长度修改。
比如:
data() {
return {
list: ['a', 'b', 'c']
};
},
methods: {
updateItem() {
this.list[0] = 'x'; // 这个修改不是响应式的,视图不会更新
this.list.length = 2; // 这个修改也不是响应式的
}
}解决方法:
- 用Vue.set(array, index, value)或者this.$set(array, index, value)
- 或者,用数组的splice方法
- 或者,替换整个数组
// 方法1:用Vue.set
this.$set(this.list, 0, 'x');
// 方法2:用splice
this.list.splice(0, 1, 'x');
this.list.splice(2); // 截取前2个,相当于length=2
// 方法3:替换数组
this.list = ['x', 'b', 'c'];Vue对数组的以下方法做了包裹,这些方法是响应式的:
- push()
- pop()
- shift()
- unshift()
- splice()
- sort()
- reverse()
用这些方法修改数组,视图会自动更新。
4. 异步更新队列
Vue更新DOM是异步的,只要侦听到数据变化,Vue将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个watcher被多次触发,只会被推入到队列中一次。
比如:
methods: {
update() {
this.message = 'new message';
console.log(this.$el.textContent); // 这里还是旧的message,因为DOM还没更新
}
}这里,修改了message之后,立即读取DOM的textContent,还是旧的值,因为DOM更新是异步的,还没执行。
解决方法:
- 用Vue.nextTick(callback)或者this.$nextTick(callback),在DOM更新后执行回调
methods: {
update() {
this.message = 'new message';
this.$nextTick(function() {
console.log(this.$el.textContent); // 这里是新的message,DOM已经更新
});
}
}这个坑,我刚上手的时候踩过,修改了数据,立即操作DOM,发现不对,调试了半天,才发现是异步更新的问题。
三、组件通信
组件是Vue.js的核心概念,组件通信也是容易踩坑的地方。
1. props传递数据
父组件通过props向子组件传递数据:
<!-- 父组件 -->
<template>
<child :message="parentMessage"></child>
</template>
<!-- 子组件 -->
<template>
<div>{{ message }}</div>
</template>
<script>
export default {
props: ['message']
};
</script>这里,有几个容易踩坑的地方:
- props是单向数据流:父组件的数据变化,会流向子组件,但是,子组件不能直接修改props,否则会警告。如果子组件需要修改,应该用data或computed。
- props命名:在模板中,props用kebab-case(短横线分隔),在JS中用camelCase(驼峰)。
- props验证:可以对props进行验证,指定类型、是否必填、默认值、自定义验证函数。
props: {
message: {
type: String,
required: true,
default: 'default',
validator: function(value) {
return value.length > 0;
}
}
}2. 事件通信
子组件通过$emit向父组件发送事件,父组件通过v-on监听事件:
<!-- 子组件 -->
<button @click="sendMessage">发送</button>
<script>
export default {
methods: {
sendMessage() {
this.$emit('child-event', 'hello from child');
}
}
};
</script>
<!-- 父组件 -->
<child @child-event="handleEvent"></child>
<script>
export default {
methods: {
handleEvent(message) {
console.log(message); // 'hello from child'
}
}
};
</script>这里,容易踩坑的地方:
- 事件名也是kebab-case(在模板中),camelCase(在JS中)
- $emit的第一个参数是事件名,后面的参数是传递的数据
- 父组件监听事件,用v-on或@,回调函数的参数就是子组件传递的数据
3. 兄弟组件通信
兄弟组件之间通信,有几种方式:
- 通过父组件中转:子组件A发送事件给父组件,父组件修改数据,通过props传给子组件B
- 通过事件总线(EventBus):创建一个空的Vue实例作为事件总线,组件之间通过事件总线通信
- 通过Vuex:用Vuex做状态管理,共享状态
事件总线的方式:
// event-bus.js
import Vue from 'vue';
export const EventBus = new Vue();
// 组件A发送事件
import { EventBus } from './event-bus.js';
EventBus.$emit('event', 'data');
// 组件B监听事件
import { EventBus } from './event-bus.js';
EventBus.$on('event', function(data) {
console.log(data);
});这里,容易踩坑的地方:
- 事件总线的事件,需要在组件销毁时移除监听,否则会导致内存泄漏,重复触发
- 用$on监听,用$off移除
export default {
created() {
EventBus.$on('event', this.handleEvent);
},
beforeDestroy() {
EventBus.$off('event', this.handleEvent);
},
methods: {
handleEvent(data) {
console.log(data);
}
}
};这个坑,我踩过,组件切换的时候,事件没有移除,导致事件被触发多次,调试了很久才发现。
4. 父组件访问子组件
父组件可以通过ref访问子组件实例:
<child ref="childRef"></child>
<script>
export default {
methods: {
accessChild() {
this.$refs.childRef.someMethod(); // 调用子组件的方法
console.log(this.$refs.childRef.someData); // 访问子组件的数据
}
}
};
</script>这里,容易踩坑的地方:
- ref在DOM渲染后才可用,不能在created或beforeMount中访问,应该在mounted或之后访问
- 如果ref用在v-for中,$refs是一个数组
- 尽量不要用ref直接操作子组件,应该用props和事件通信,ref只在必要时使用
四、生命周期
Vue组件有生命周期钩子,也是容易踩坑的地方。
Vue 2.0的生命周期:
- beforeCreate:实例初始化之后,数据观测和事件配置之前调用
- created:实例创建完成后调用,数据观测、属性和方法运算、事件回调都已完成,但是DOM还没生成,$el还不可用
- beforeMount:挂载开始之前调用,render函数首次被调用
- mounted:挂载完成后调用,DOM已生成,$el可用,但是不保证所有子组件都挂载完成
- beforeUpdate:数据更新时调用,发生在虚拟DOM重新渲染和打补丁之前
- updated:数据更新后调用,组件DOM已更新,但是不保证所有子组件都重绘完成
- beforeDestroy:实例销毁之前调用,实例仍然完全可用
- destroyed:实例销毁后调用,所有事件监听器被移除,子实例被销毁
容易踩坑的地方:
- created vs mounted:created中数据已可用,但是DOM还没生成,不能操作DOM;mounted中DOM已生成,可以操作DOM。
- mounted不保证子组件挂载完成:如果需要等所有子组件挂载完成,用this.$nextTick。
- updated不保证子组件重绘完成:同理,用this.$nextTick。
- 定时器和事件监听器要在beforeDestroy中清理:否则会导致内存泄漏。
export default {
data() {
return {
timer: null
};
},
mounted() {
this.timer = setInterval(() => {
console.log('timer');
}, 1000);
window.addEventListener('resize', this.handleResize);
},
beforeDestroy() {
clearInterval(this.timer); // 清理定时器
window.removeEventListener('resize', this.handleResize); // 移除事件监听器
},
methods: {
handleResize() {
console.log('resize');
}
}
};这个坑,我也踩过,组件销毁了,定时器还在跑,导致内存泄漏,页面越来越卡,后来才发现是定时器没清理。
五、Vuex状态管理
当应用复杂的时候,组件之间的状态共享变得困难,这时候就需要Vuex。
Vuex是Vue.js的官方状态管理库,采用集中式存储管理应用的所有组件的状态。
Vuex的核心概念:
- State:状态,存储数据
- Getter:计算属性,从state派生出状态
- Mutation:修改状态的方法,必须是同步的
- Action:提交mutation,可以包含异步操作
- Module:模块,分割store
容易踩坑的地方:
1. Mutation必须是同步的
Mutation必须是同步的,不能在mutation中做异步操作,否则devtools无法追踪状态变化。
异步操作应该放在action中,action提交mutation。
// 错误:mutation中做异步操作
mutations: {
updateData(state) {
setTimeout(() => {
state.data = 'new data';
}, 1000);
}
}
// 正确:action中做异步操作,提交mutation
mutations: {
updateData(state, data) {
state.data = data;
}
},
actions: {
updateDataAsync({ commit }) {
setTimeout(() => {
commit('updateData', 'new data');
}, 1000);
}
}2. 组件中提交mutation和dispatch action
组件中,用this.$store.commit提交mutation,用this.$store.dispatch分发action。
// 提交mutation
this.$store.commit('updateData', 'data');
// 分发action
this.$store.dispatch('updateDataAsync', 'data');也可以用mapMutations和mapActions辅助函数:
import { mapMutations, mapActions } from 'vuex';
export default {
methods: {
...mapMutations(['updateData']),
...mapActions(['updateDataAsync'])
}
};3. 模块化的命名空间
当store模块化的时候,如果开启了命名空间(namespaced: true),那么commit、dispatch、getter都需要加上命名空间前缀。
// 模块
const userModule = {
namespaced: true,
state: { name: 'test' },
mutations: {
updateName(state, name) {
state.name = name;
}
}
};
// 组件中
this.$store.commit('user/updateName', 'new name'); // 加上命名空间前缀
this.$store.state.user.name; // 访问模块的state这个坑,我刚用Vuex的时候踩过,没加命名空间前缀,mutation不生效,调试了半天。
六、Vue Router路由
Vue Router是Vue.js的官方路由管理器,用于构建单页应用(SPA)。
容易踩坑的地方:
1. 路由模式
Vue Router有两种模式:
- hash模式:URL带#,比如http://example.com/#/user,默认模式,兼容性好
- history模式:URL不带#,比如http://example.com/user,需要后端配置支持,否则刷新页面会404
用history模式,需要后端配置,把所有路由都指向index.html:
# Nginx配置
location / {
try_files $uri $uri/ /index.html;
}这个坑,我踩过,用了history模式,但是后端没配置,刷新页面404,排查了很久。
2. 路由跳转
路由跳转,有几种方式:
- 声明式:
<router-link :to="{ path: '/user' }">用户</router-link> - 编程式:
this.$router.push('/user')、this.$router.replace('/user')、this.$router.go(-1)
容易踩坑的地方:
- push会向history添加新记录,replace不会添加新记录,替换当前记录
- 路由参数变化,组件会复用,不会重新创建,生命周期钩子不会重新调用,如果需要响应参数变化,用watch监听$route,或者用beforeRouteUpdate导航守卫
// 监听路由参数变化
watch: {
'$route'(to, from) {
// 路由变化,重新加载数据
this.loadData(to.params.id);
}
}这个坑,我踩过,路由参数变了,但是组件没更新,数据还是旧的,后来才发现是组件复用的问题。
3. 导航守卫
Vue Router有导航守卫,用于在路由跳转时做一些操作,比如登录验证。
- 全局守卫:beforeEach、beforeResolve、afterEach
- 路由独享守卫:beforeEnter
- 组件内守卫:beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave
比如,登录验证:
router.beforeEach((to, from, next) => {
if (to.meta.requiresAuth && !isLoggedIn()) {
next('/login'); // 未登录,跳转到登录页
} else {
next(); // 登录了,继续
}
});容易踩坑的地方:
- 守卫中必须调用next(),否则路由不会继续
- next()可以传参数,比如next('/login')跳转到指定路由,next(false)中断导航
- beforeRouteEnter中不能访问this,因为组件还没创建,可以用next(vm => {})访问组件实例
七、其他常见踩坑
除了上面说的,还有一些其他常见的踩坑。
1. v-for的key
用v-for渲染列表的时候,必须加key,而且key要唯一,稳定,不要用index作为key(如果列表会变化的话)。
<!-- 正确 -->
<div v-for="item in list" :key="item.id">{{ item.name }}</div>
<!-- 不推荐(如果列表会变化) -->
<div v-for="(item, index) in list" :key="index">{{ item.name }}</div>用index作为key,如果列表中间插入或删除元素,后面的元素的index都会变,导致key变化,组件会重新渲染,性能差,而且可能导致状态错乱。
2. v-if和v-for不要一起用
v-if和v-for不要用在同一个元素上,因为v-for的优先级比v-if高,所以v-if会在每个v-for循环中都执行,性能差,而且逻辑混乱。
如果需要条件渲染列表,应该用computed先过滤列表,或者把v-if放在外层元素。
<!-- 不推荐 -->
<div v-for="item in list" v-if="item.visible">{{ item.name }}</div>
<!-- 推荐:用computed过滤 -->
<div v-for="item in visibleList" :key="item.id">{{ item.name }}</div>
<script>
computed: {
visibleList() {
return this.list.filter(item => item.visible);
}
}
</script>3. 表单输入绑定
用v-model绑定表单输入,有几个修饰符:
- .lazy:在change事件后同步,而不是input事件
- .number:自动转为数字
- .trim:自动过滤首尾空格
<input v-model.lazy="message">
<input v-model.number="age" type="number">
<input v-model.trim="name">checkbox和radio的绑定,也需要注意:
- checkbox:单个checkbox绑定布尔值,多个checkbox绑定数组
- radio:绑定value,选中时v-model的值等于value
4. 样式作用域
用<style scoped>,样式只作用于当前组件,不会污染全局。但是,scoped样式,对子组件的根元素有效,对子组件的内部元素无效。
如果需要修改子组件内部的样式,需要用深度选择器(>>>或/deep/):
<style scoped>
/* 只作用于当前组件 */
.title {
color: red;
}
/* 深度选择器,作用于子组件内部 */
>>> .child-title {
color: blue;
}
</style>5. 组件命名
组件命名,有两种方式:
- PascalCase(大驼峰):MyComponent
- kebab-case(短横线):my-component
在模板中,用PascalCase或kebab-case都可以,但是,在DOM模板中(直接写在HTML里),只能用kebab-case,因为HTML不区分大小写。
注册组件的时候,用PascalCase:
Vue.component('my-component', {
// ...
});八、最佳实践
最后,总结一些Vue.js 2.0的最佳实践。
1. 组件化
- 组件尽量小,职责单一,一个组件只做一件事
- 通用组件抽离,复用代码
- 组件命名清晰,见名知意
- props和事件定义清晰,组件接口明确
2. 状态管理
- 简单的状态,用组件data和props/events
- 复杂的跨组件状态,用Vuex
- 不要把所有状态都放Vuex,只放需要共享的状态
- Vuex的mutation要纯函数,同步;action处理异步
3. 性能优化
- 合理使用v-if和v-show,频繁切换用v-show,不频繁切换用v-if
- 列表渲染加key,key唯一稳定
- 大数据列表用虚拟滚动
- 合理使用computed缓存计算结果
- 组件懒加载,路由懒加载
- 生产环境构建,压缩代码
4. 代码规范
- 遵循Vue风格指南
- 组件data必须是函数
- props定义尽量详细,指定类型、默认值、验证
- 模板简洁,复杂逻辑放computed或methods
- 样式用scoped,避免污染全局
5. 调试工具
- 用Vue Devtools调试,查看组件树、状态、事件、路由
- 用Vue Devtools的时间旅行调试,追踪状态变化
- 控制台报警告和错误,及时修复
九、写在最后
Vue.js 2.0上手踩坑记:那些让我熬夜的问题。
Vue.js是一个优秀的前端框架,简单易学,轻量高效,但是,在上手的过程中,还是会踩很多坑,特别是响应式原理、组件通信、生命周期、Vuex、Vue Router这些核心概念,需要深入理解,才能用好。
我踩过的这些坑,希望能帮助大家少走弯路,更快地上手Vue.js,写出优雅高效的代码。
Vue.js还在不断发展,新的特性不断加入,我们也要不断学习,跟上技术的步伐。
最后,用一句话结尾:
"框架是工具,不是目的。理解框架的原理,才能真正用好框架。"
愿大家都能用好Vue.js,构建出优秀的前端应用。
评论(0)
暂无评论,快来抢沙发~
评论功能仅对会员开放,请先登录
登录