前言
欢迎关注同名公众号《熊的猫
》,文章会同步更新,也可快速加入前端交流群!
最近在协助小伙伴解决问题时,在项目中都会遇到一些 “砍树型” 的写法,这些写法容易让后续 简单 的需求变得 复杂,都说 "前人栽树后人乘凉",但项目中有些写法是真的让后人乘不了凉的,甚至还得被迫加入 “砍树队伍”。
本篇文章就列举一些,在 vue 项目中的 “砍树型” 的写法,以及分析一下如何写才更合适 “栽树”,如果你有更好的方案,欢迎在评论区分享!!!
砍树 & 栽树
由于项目源码不便于直接展示,下面会使用同等的代码实例来替代。
其项目技术栈为:vue2 + vue-class-component + vue-property-decorator + typescript。
滥用 watch
砍树型写法
@Watch('person', { deep: true })
doSomething(){}
@Watch('person.name', { deep: true })
doSomething(){}
@Watch('person.age', { deep: true })
doSomething(){}
@Watch('person.hobbies', { deep: true })
doSomething(){}
第一次看到这个写法我有点迷茫,但想了想好像也不难理解:
- 首先 person.x 的部分监听 是为了处理针对不同属性值发生修改时要执行的特定逻辑
- 而针对 person 的整体监听 是为了执行属于公共部分的逻辑
因此,上面的写法就只是相当于只是少写了几个 if 的条件分支罢了,更何况还都用了深度监听,而实际上这种 简化方式 在 vue 内部会实例化出多个 Watcher 实例,如下:
栽树型写法
针对上述写法,如果说后续需要追加不同属性变更时的新逻辑,会有两种情况:
看懂的人,会使用一样的 person.x 的部分监听 方式去添加新逻辑
- 实际上一个 Watcher 就可以解决,没必要实例化多个 Watcher
看不懂的人,可能会把新逻辑杂糅在 person 的整体监听 的公共逻辑中
- 还得注意添加执行时机条件的判断,很容易出问题
总之,这两种情况都并不好,因此更推荐原本 if 的写法:
@Watch('person', { deep: true })
doSomething(newVal, oldVal){
doSomethingCommon() // 公共逻辑
if(newVal.name !== oldVal.name){
doSomethingName() // 逻辑抽离
}
if(newVal.age !== oldVal.age){
doSomethingAge() // 逻辑抽离
}
...
}
不合理使用 async/await
砍树型写法
记得当时有反馈前端视图更新太慢,因为后端通过日志查看接口响应速度还是很快的,于是查看前端代码时发现类似如下的使用:
async mounted(){
await this.request1(); // 耗时接口
this.request2(); // request2 需要依赖 request1 的请求结果
this.request3(); // request3 不需要依赖任何的请求结果
this.request4(); // request4 不需要依赖任何请求结果
}
这种写法就导致了 request3 和 request4 虽然不需要依赖前面异步请求结果,但是必须要等待耗时操作完成才能请求,而视图更新又必须等待接口调用完成。
上述写法可能在 开发 和 测试 环境没有太明显的影响,但是在 生产环境,这个影响就会被放大,因为不同环境数据量不同,所接口响应速度更不同,并且用户可能不会注意你的数据是否准备完成就进行相应操作,这个时候就有可能出现问题。
栽树型写法
为了更快的得到视图更新,针对以上写法可进行如下调整:
将无关相互依赖的请求前置在 await 之前
这种方式适合使用的场景就是 request1 本身还需要再其他地方单独调用,因此其内部不适合在存放额外的逻辑
async mounted(){ this.request3(); this.request4(); await this.request1(); // 耗时接口 this.request2(); // request2 需要依赖 request1 的请求结果 }
将相互依赖的请求在统一在内部处理
例如,将 request2 放置到 request1 的具体实现中,这种方式适用于 request1 和 request2 间在任何情况下都有紧密联系的情况下,当然也可以在 request1 内通过 条件判断 决定是否要执行 request2
async mounted(){ this.request3(); this.request4(); this.request1(); // 耗时接口 } async request1(){ const res = await asyncReq(); this.request2(res); // request2 需要依赖 request1 的请求结果 }
同时还需要注意的是,虽然 request2 需要依赖 request1 的结果,但是对于视图更新来说,却没有必要等待 request2 请求完成后再去更新视图,也就是说,request1 请求结束后有需要更新视图的部分就可以先更新,这样视图更新时机就不会延后。
组件层层传参
砍树型写法
项目中有一个模版切换的功能,而这个模版功能封装成了一个组件,在外部看起来是 Grandpa 组件,实际上其内部包含了 Parents 组件,而最底层使用的是 Son 组件:
// 顶层组件
<Grandpa :data="data" @customEvent="customEvent" />
// 中间层组件
<Parents :data="data" @customEvent="customEvent" />
// 底层组件
<Son :data="data" @customEvent="customEvent" />
由于底层的 Son 组件 需要使用到 props data 和 自定义事件 customEvent,在代码中通过逐层传递的方式来实现,甚至在 Grandpa 组件 和 Parents 组件 中都有对 props.data 做 deepClone 深克隆 且修改后在往下层传递。
缺点很明显了:
重复定义 props
- 需要分别在 Grandpa、Parents、Son 三个组件中定义相关的 props 和 event
props 的修改来源不确定
- 由于 Grandpa、Parents 组件都对 props.data 有修改,在出现问题需要排查时可能都要排查 Grandpa、Parents 组件
栽树型写法
上面的写法属实繁琐且不优雅,实际上可以通过 $attrs
和 $listeners
来实现 属性和事件透传,如下:
// 顶层组件
<Grandpa :data="data" @customEvent="customEvent" />
// 中间层组件
<Parents v-bind="$attrs" v-on="$listeners" />
// 底层组件
<Son v-bind="$attrs" v-on="$listeners" />
而其中涉及到直接通过 deepClone 深克隆 的原因应该是为了便于 直接 增加/删除 props.data 中的属性,实际上应该在 props 提供层 提供修改的方法。
没有必要的响应式数据
砍树型写法
很多时候在 Vue 中我们需要在 <template>
模版中使用 常量数据,但并不会对其进行修改,如果我们没有在 Vue 组件实例上定义,那么在是无法访问到的,于是就是有如下用法:
<template>
<div class="app">
<ul>
<li v-for="num in constantData" :key="num">{{ num }}</li>
</ul>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { Component } from 'vue-property-decorator';
@Component({})
export default class APP extends Vue {
// 常量数据,只用不改,无需响应式,但会转成响应式
ConstantData: number[] = [1, 2, 3, 4, 5];
}
</script>
然而这样的写法虽然可以达到目的,但是会将其转换为 无必要的响应式数据,而在 Vue2 中越复杂的对象转换为 响应式对象 就越繁琐,毕竟需要层层转换等,而以上给的例子还只是简单的内容。
栽树型写法
显然,我们根本不需要使用其响应式的特性,只需要 ConstantData 能够被模版正常访问到即可,那么我们可以使用如下的写法:
<script lang="ts">
import Vue from 'vue';
import { Component } from 'vue-property-decorator';
@Component({})
export default class APP extends Vue {
// 常量数据,只用不改,无需响应式,不会转成响应式
constantData: readonly number[] = Object.freeze([1, 2, 3, 4, 5]);
}
</script>
原理也很简单,在 vue 的源码中有如下的判断:
即只要保证目标对象属性描述符的 configurable = false,就能够保证其不会被转成响应式数据,而这我们就可以使用 Object.freeze / Object.seal 来实现,如下:
易变的 key
砍树型写法
在需要实现通过 v-for 实现 列表渲染 时,大多数人喜欢直接使用 index 作为唯一 key,如下:
<li v-for="(item, index) in mockData" :key="index">
<!-- other content -->
<button @click="deleteItem(index)">删除</button>
</li>
特别是带有 删除/移动 操作的列表,使得 index 变成 易变的 key,如果此时还不使用 唯一 key 值,那么在更新阶段会进行一些 无意义的删除/创建,这会带来性能问题。
只要列表内容足够复杂,例如其中包含 Form 表单、Table 表格、ECharts 可视化图表 等等,在更新阶段的性能会表现得尤为重要,一个操作可能就导致 页面更新出现卡顿 等问题,虽然 key 并不是唯一原因,但还是尽量 使用唯一值来作为 key 值。
栽树型写法
但是并不是每个数据都会有唯一值可咋整?
那我们可以自己生成 key 值,而还可以使用 index 来作为唯一值,让它不易变就好了:
function generateKey(data, keyIndex){
data.forEach((item, index) => {
item.key = keyIndex !== undefined ? keyIndex++ : index;
});
}
new Vue({
el: '#demo',
data: {
mockData: generateKey([
{
value: '1',
...
},
{
value: '2',
...
},
])
},
methods: {
deleteItem(index){
this.mockData.splice(index, 1);
}
}
})
template 模版中的复杂表达式
砍树型写法
这里以 v-if、v-show、class 等来进行演示,如下:
// 情况一
<div v-if="condition1 && condition2 && method3() && computed1">
<!-- content -->
</div>
// 情况二
<div v-show="condition1 && condition2 && method3() && computed1">
<!-- content -->
</div>
// 情况三
<div :class="['class1', condition1 ? 'class2' : '', condition1 && condition2 ? 'class3' : '']">
<!-- content -->
</div>
这种写法只会让这个条件判断写得越来越长,虽说支持在模版中写表达式,但是如此 长且复杂 的表达式直接写在 <template>
中属实不合适,应该要尽量精简,修改条件时也不应该跑到模版中去修改。
栽树型写法
在模版中的表达式要保持精简,换句话说就是把表达式的结果放在模版中就好了,那么可想而知就可以使用 computed 和 method 了。
针对 v-if 和 v-show 可以使用如下方式:
针对 class 可以选择将 不变的 和 可变的 进行 分开 或 不分开 处理:
// 方式一:分开
<div
class="class1"
:class="divConditionClass">
<!-- content -->
</div>
new Vue({
el: '#demo',
data: {},
computed: {
divConditionClass(){
return {
'class2' : condition1,
'class3': condition1 && condition2
};
},
}
})
// 方式二:不分开
<div
:class="divClass">
<!-- content -->
</div>
new Vue({
el: '#demo',
data: {},
computed: {
divClass(){
return {
'class1': true,
'class2' : condition1,
'class3': condition1 && condition2
};
},
}
})
v-if 和 v-for 共用
砍树型写法
v-if 和 v-for 一起使用已经老生常谈的问题了,但是还是会有人这样使用,如下:
先抛开 v-if 和 v-for 渲染优先级所带来的性能消耗不讲,单单是这个 红色波浪线 的提示难道还不够明显吗?
不过值得注意的是,在 vue3 中 v-if 和 v-for 同时使用时,v-if 的优先级会比 v-for 更高.
栽树型写法
v-if 和 v-for 无非就是为了控制 显示和隐藏,既然如此只要将不需要渲染的数据内容过滤出来即可,而这不就可以通过 computed 和 method 来实现了,如下:
这里可能有人会说也可以使用 vue 提供的 filters 过滤器,但是过滤器只能用在两个地方:
- 双花括号插值
{{ }}
v-bind
表达式
也就是说如果你要在 v-for 中使用 过滤器 就会发生错误,如下:
// 错误的使用过滤器
<li v-for="(item, index) in mockData | filterData" :key="item.key">
{{ item.value }}
</li>
!important 重写样式
砍树型写法
当需要调整些样式问题时,发现改动无效,一排查发现 !important 卡得死死的,如下:
直接使用 !important 强制覆盖样式虽然方便,但是这卡得也太死了,而且还是不限制对应组件内容,直接全局覆盖,这样其他人想去改动估计都得掂量掂量,会不会影响某个页面的视图效果。
栽树型写法
调整样式时先区分是 局部样式 还是 全局样式:
如果是 局部样式 那么最好使用 scoped,或者是 页面顶层选择器 限定一下范围
<style lang="less" scoped> @import './index.less'; </style>
如果是 全局样式 不要偷懒写在 当前组件 中,因为这样使得全局样式分离了,将来修改时也不好查找,如下:
// 注意没有 scoped <style lang="less"> @import './index.less'; </style>
当然还有一种情况是 B 组件 的样式,在 A 组件 中需要调整,在其他地方使用 B 组件 时不需要调整,但此时如果直接在 A 组件 的 scoped 样式中修改时可能不生效,这个时候需要在 scoped 限制外去调整 B 组件 的样式,那么可以这么写:
// A 组件中
<style lang="less" scoped>
@import './index.less';
</style>
<style lang="less">
// 也可以抽离出去
.A {
.B {
...
}
}
</style>
单文件内容过多
砍树型写法
一个单文件的内容实在是太多、太长,如下:
太长的内容 出现问题不好排查、新增逻辑不好加,更何况是那些涉及到上下联动的逻辑。
那么是什么原因导致这么长呢?
因为 该抽离的内容没有抽离,例如:
<template>
模版的内容中 表单、表格、弹窗等内容没有很好抽离<script>
部分state
的初始化声明赋值等过长,例如与表单、表格
相关的 state- 相同的方法,总是要在不同的
.vue
文件重复声明
栽树型写法
那怎么抽离呢?
<template>
模版部分抽离:
复杂或结构长的部分可以封装成组件
- 例如,
表单、表格
的渲染就没必要一个结构一个结构的写,应该要通过JSON Schema
的方式来实现,在Vue
中正好可以配合slot
插槽 实现自定义内容
- 例如,
复杂的弹窗内容也可以组件化
- 在上述的文件中,发现和弹窗相关的内容有 10 个,也就是写了 10 个 Dialog,如果后续还有其他不同的弹窗内容,结果可想而知,除此之外和 Dialog 相关的 state 是不是又得分别定义,因此十分不推荐这种方式
- 仔细想想,不同弹窗变的是弹窗内容和一些附带属性而已,因此 Dialog 可以只写一个,这意味着和 Dialog 相关的 state 也只需定义一个,针对不同的弹窗内容可以封装成组件,根据情况来渲染,也可以配合
<Component :is="name" />
实现
<script>
部分抽离:
组件化
- 这一点不难理解,封装成组件后对应的逻辑也从当前组件中可以抽离出去
mixins 混入
- 虽说多个 mixins 会导致数据来源的不确定性,但不影响它做逻辑的抽离
自定义指令 directives
- 如果有一段逻辑是需要操作 真实 DOM,并且这段逻辑整体上是值得被复用的,那么可以将其封装成全局自定义指令
extend 继承
- 由于这个项目支持使用 class 的形式,那么一些 重复的属性、方法 就可以通过 extend 关键字来继承,无非就是定义某个公共的类,然后其他组件去继承这个类即可
抽离过长的 state 初始值
- 对于
state
的初始化赋值比较长的内容,可以将其抽离到相关独立文件中,简化主文件中不必要的代码长度,更清晰只观
- 对于
最后
欢迎关注同名公众号《熊的猫
》,文章会同步更新,也可快速加入前端交流群!
好了,暂时先写到这里吧!毕竟是写不完的!
虽说做 Code Review 的时候,大家都说影响开发速度,毕竟开发时间限制在那,但是不做 Code Review 的时候,不同的开发者写法各式各样,重点是还能绕过规范检查工具,后期自己或他人维护时的难易程度可想而知,所以大家还是多多 “栽树” 吧!
希望本文对你有所帮助!!!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。