【51CTO.com原创稿件】
前言
本文主要介绍日常项目开发过程中的一些技巧,帮助大家规避错误的同时还能提高应用的性能。以下是我总结的一些平时工作中的经验。
在v-if/v-if-else/v-else中使用key
如果一组v-if 与v-else的元素类型相同,最好使用属性key。这是因为Vue2.0引入虚拟DOM,为了避免不必要的DOM操作,虚拟DOM在虚拟节点映射到视图过程中,将虚拟节点与上一次渲染视图所使用的旧虚拟节点做对比,找出真正需要更新的节点来进行DOM操作。但有时如果两个本不相同的节点被识别为相同,便会出现意料之外的问题。我们看下面的一个例子:
- // 这种写法会出bug
- <div v-if="flag">
- <label>浪里行舟</label>
- <input type="text" />
- </div>
- <div v-else>
- <label>前端工匠</label>
- <input type="text" />
- </div>
- <button @click="flag = !flag">切换</button>
如果添加了属性key,那么在对比虚拟DOM时,则会认为它们是两个不同的节点,于是会将旧元素移除并在相同位置添加新元素,从而避免漏洞的出现。
- // 最佳写法
- <div v-if="flag">
- <label>浪里行舟</label>
- <input key="1" type="text" />
- </div>
- <div v-else>
- <label>前端工匠</label>
- <input key="2" type="text" />
- </div>
- <button @click="flag = !flag">切换</button>
v-for循环中不要使用index作为key
我们会给列表渲染设置属性key,这个key属性主要用在虚拟DOM算法上,在对比新旧虚拟节点时辨识虚拟节点。但如果key用得不合理,就会出现bug,比如我们使用index作为key(见下面例子),核心代码如下:
- <div class="border">
- <Children v-for="(key, index) in list" :key="index">//使用index作为key
- <button @click="() => handleDelete(key)">删除</button>
- </Children>
- <button @click="handleAdd">添加</button>
- </div>
- ......
- handleAdd() {
- this.list.push(key++);
- },
- handleDelete(key) {
- const index = this.list.findIndex(k => k === key);
- this.list.splice(index, 1);
- }
上例中,我们想删除第二个输入框,却误删了第三个输入框,这因为当使用splice()方法删除数组的某个元素时数组的index会被重新索引,造成数组的最后一个index丢失,从而会使虚拟DOM的最后一个结点(key)丢失,造成无论删除哪个结点都会误删除最后一个结点的bug。但如果我们使用传入的key作为key,就可以避免这种问题出现。
- <div class="border">
- <Children v-for="key in list" :key="key">
- <button @click="() => handleDelete(key)">删除</button>
- </Children>
- <button @click="handleAdd">添加</button>
- </div>
简单暴力的router key
我们在项目开发时,可能会遇到这样问题:当页面切换到同一个路由但不同参数地址时,比如/detail/1,跳转到/detail/2,页面跳转后数据竟然没更新?路由配置如下:
- {
- path: "/detail/:id",
- name:"detail",
- component: Detail
- }
这是因为vue-router会识别出两个路由使用的是同一个组件从而进行复用,并不会重新创建组件,而且组件的生命周期钩子自然也不会被触发,导致跳转后数据没有更新。那我们如何解决这个问题呢? 我们可以为router-view组件添加属性key,例子如下:
- <router-view :key="$route.fullpath"></router-view>
这种办法主要是利用虚拟DOM在渲染时候通过key来对比两个节点是否相同,如果key不相同,就会判定router-view组件是一个新节点,从而先销毁组件,然后再重新创建新组件,这样组件内的生命周期会重新触发。
路由懒加载
当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。 配合webpack支持的路由懒加载方法有:
-
这种方法比较通用,而且支持性好
- resolve => require([./Foo], resolve)
-
这种写法是官方文档推荐的,如下:
- const Foo = () => import('./Foo')
接下来我们以官方文档的写法为例,对比这两种写法:
- // 非懒加载写法
- import Vue from 'vue'
- import Router from 'vue-router'
- import Home from 'pages/home'
- ...
-
- Vue.use(Router)
-
- export default new Router({
- routes: [
- {
- path: '/',
- name: 'home',
- component: Home
- }
- ...
- ]
- })
推荐以下写法,路由懒加载可以帮我们在进入首屏时不用加载过多的资源,从而减少首屏加载速度。
- // 路由懒加载写法
- import Vue from 'vue'
- import Router from 'vue-router'
-
- // 其它都不用变,就是这么简单
- const Home = () => import('./home')
- ...
-
- Vue.use(Router)
-
- export default new Router({
- routes: [
- {
- path: '/',
- name: 'home',
- component: Home
- }
- ...
- ]
- })
不要在使用v-for的同一元素上使用v-if
Vue官方文档强烈建议永远不要把 v-if 和 v-for 同时用在同一个元素上。一般我们在两种常见的情况下会倾向于这样做:
-
1)为了过滤一个列表中的项目 (比如
v-for="user in users" v-if="user.isActive"
)。在这种情形下,请将 users 替换为一个计算属性 (比如 activeUsers),让其返回过滤后的列表(见下面例子)。
- // 第一种情形 反例
- <ul>
- <li
- v-for="user in users"
- v-if="user.isActive"
- :key="user.id"
- >
- {{ user.name }}
- </li>
- </ul>
当 Vue 处理指令时,v-for 比 v-if 具有更高的优先级,所以哪怕我们只渲染出一小部分用户的元素,也得在每次重渲染的时候遍历整个列表,不论活跃用户是否发生了变化。我们可通过将其更换为在如下的一个计算属性上遍历:
- // 好例子
- computed: {
- activeUsers: function () {
- return this.users.filter(function (user) {
- return user.isActive
- })
- }
- }
- <ul>
- <li
- v-for="user in activeUsers"
- :key="user.id"
- >
- {{ user.name }}
- </li>
- </ul>
-
2)为了避免渲染本应该被隐藏的列表 (比如
v-for="user in users" v-if="shouldShowUsers"
)。这种情形下,请将 v-if 移动至容器元素上 (比如 ul, ol)(见下面例子)。
- // 第二种情形 反例
- <ul>
- <li
- v-for="user in users"
- v-if="shouldShowUsers"
- :key="user.id"
- >
- {{ user.name }}
- </li>
- </ul>
更新为:
- // 好例子
- <ul v-if="shouldShowUsers">
- <li
- v-for="user in users"
- :key="user.id"
- >
- {{ user.name }}
- </li>
- </ul>
过滤器让数据处理更便利
Vue.js 允许你自定义过滤器,它的用法其实是很简单,但是可能有些朋友没有用过,接下来我们介绍下:
1.理解过滤器
-
功能:对要显示的数据进行特定格式化后再显示。
-
注意:过滤器并没有改变原本的数据,需要对展现的数据进行包装。
-
使用场景:双花括号插值和 v-bind 表达式 (后者从 2.1.0+ 开始支持)。
2.定义过滤器
可以在一个组件的选项中定义本地的过滤器:
- filters: {
- capitalize: function (value) {
- if (!value) return ''
- value = value.toString()
- return value.charAt(0).toUpperCase() + value.slice(1)
- }
- }
也可以在创建 Vue 实例之前全局定义过滤器:
- Vue.filter('capitalize', function (value) {
- if (!value) return ''
- value = value.toString()
- return value.charAt(0).toUpperCase() + value.slice(1)
- })
3.使用过滤器
使用方法也简单,即在双花括号中使用管道符(pipeline) |隔开:
- <!-- 在双花括号中 -->
- <div>{{ myData| filterName}}</div>
- <div>{{ myData| filterName(arg)}}</div>
- <!-- 在 v-bind 中 -->
- <div v-bind:id="rawId | formatId"></div>
接下来我们看个如何使用过滤器格式化日期的例子:
- <div>
- <h2>显示格式化的日期时间</h2>
- <p>{{ date }}</p>
- <p>{{ date | filterDate }}</p>
- <p>年月日: {{ date | filterDate("YYYY-MM-DD") }}</p>
- </div>
- ......
- filters: {
- filterDate(value, format = "YYYY-MM-DD HH:mm:ss") {
- console.log(this)//undefined 过滤器没有this指向的
- return moment(value).format(format);
- }
- },
- data() {
- return {
- date: new Date()
- };
- }
能用computed的尽量用computed代替 watch
很多时候页面会出现 watch 的滥用而导致一系列问题的产生,而通常更好的办法是使用 computed 属性,先从一张图区别它们有什么区别?
从上面流程图中,我们可以看出它们之间的区别:
-
watch:监测的是属性值, 只要属性值发生变化,其都会触发执行回调函数来执行一系列操作。
-
computed:监测的是依赖值,依赖值不变的情况下其会直接读取缓存进行复用,变化的情况下才会重新计算。
除此之外,有点很重要的区别是:计算属性不能执行异步任务,计算属性必须同步执行。也就是说计算属性不能向服务器请求或者执行异步任务。如果遇到异步任务,就交给侦听属性。watch也可以检测computed属性。总而言之,两者的区别归纳为以下两句话:
-
computed能做的,watch都能做,反之则不行
-
能用computed的尽量用computed
为啥提倡使用 computed 代替 watch,这是因为有时候可以实现同样的效果,而 computed 会更胜一筹,比如在处理多数据联动的情况下,使用 computed 会更加合理一点。
- <template>
- <div>
- <input type="text" v-model="firstName">
- <input type="text" v-model="lastName">
- <span>{{ fullName }}</span>
- <span>{{ fullName2 }}</span>
- </div>
- </template>
-
- <script>
- export default {
- data() {
- reurn {
- firstName: '',
- lastName: '',
- fullName2: ''
- }
- },
- // 使用 computed
- computed: {
- fullName() {
- return this.firstName + ' ' + this.lastName
- }
- },
- // 使用 watch
- watch: {
- firstName: function(newVal, oldVal) {
- this.fullName2 = newVal + ' ' + this.lastName;
- },
- lastName: function(newVal, oldVal) {
- this.fullName2 = this.firstName + ' ' + newVal;
- },
- }
- }
- </script>
化繁为简的Watchers
如果我们需要在组件初始化以及侦听属性变化时调用同一个方法,通常的做法像下面这样:
- watch: {
- myProperty() {
- this.doSomething();
- }
- },
- created() {
- this.doSomething();
- },
- methods: {
- doSomething() {
- console.log('doing something...');
- }
- }
尽管上面这段代码看起来没什么问题,但created钩子里面执行的方法是多余的。我们可以把所需要执行的方法放到watch里面,避免在created钩子里写重复代码,那将会在组件实例化的时候触发多一次。 那如何优化呢?代码如下:
- watch: {
- myProperty: {
- immediate: true,//表示创建组件时立马执行一次
- handler() {
- console.log('doing something...'); // 只用一次的方法没必要在methods里面声明了
- }
- }
- }
参考文章与书籍
作者介绍
浪里行舟,慕课网认证作者,前端爱好者,立志往全栈工程师发展,从事前端一年多,目前技术栈有vue全家桶、ES6以及less等,乐于分享,最近一年写了五六十篇原创技术文章,得到诸多好评!
【51CTO原创稿件,合作站点转载请注明原文作者和出处为51CTO.com】