注册
web

前人在 vue 项目中的 “砍树型“ 写法,让后人乘不了凉!

前言


最近在协助小伙伴解决问题时,在项目中都会遇到一些 “砍树型” 的写法,这些写法容易让后续 简单 的需求变得 复杂,都说 "前人栽树后人乘凉",但项目中有些写法是真的让后人乘不了凉的,甚至还得被迫加入 “砍树队伍”。


本篇文章就列举一些,在 vue 项目中的 “砍树型” 的写法,以及分析一下如何写才更合适 “栽树”,如果你有更好的方案,欢迎在评论区分享!!!


89DFA925.png


砍树 & 栽树


由于项目源码不便于直接展示,下面会使用同等的代码实例来替代。


其项目技术栈为: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 实例,如下:


image.png


image.png


image.png


栽树型写法


针对上述写法,如果说后续需要追加不同属性变更时的新逻辑,会有两种情况:



  • 看懂的人,会使用一样的 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() // 逻辑抽离
}

...
}

值得注意的是,当使用 watch 深度监听对象时,其中的 newValoldVal 的值会一致,因为此时它们指向的是 同一个对象,因此如果真的需要如上例的方式来使用,就需要提前将目标对象进行 深度克隆


因此,这两种写法到底哪种是 "栽树",哪种是 "砍树",需要见仁见智了!


946CB97F.gif


不合理使用 async/await


砍树型写法


记得当时有反馈前端视图更新太慢,因为后端通过日志查看接口响应速度还是很快的,于是查看前端代码时发现类似如下的使用:


 async mounted(){
await this.request1(); // 耗时接口
this.request2(); // request2 需要依赖 request1 的请求结果
this.request3(); // request3 不需要依赖任何的请求结果
this.request4(); // request4 不需要依赖任何请求结果
}

这种写法就导致了 request3request4 虽然不需要依赖前面异步请求结果,但是必须要等待耗时操作完成才能请求,而视图更新又必须等待接口调用完成。


上述写法可能在 开发 和 测试 环境没有太明显的影响,但是在 生产环境,这个影响就会被放大,因为不同环境数据量不同,所接口响应速度更不同,并且用户可能不会注意你的数据是否准备完成就进行相应操作,这个时候就有可能出现问题。


93DE32BE.gif


栽树型写法


为了更快的得到视图更新,针对以上写法可进行如下调整:



  • 将无关相互依赖的请求前置在 await 之前

    • 这种方式适合使用的场景就是 request1 本身还需要再其他地方单独调用,因此其内部不适合在存放额外的逻辑

     async mounted(){
    this.request3();
    this.request4();

    await this.request1(); // 耗时接口
    this.request2(); // request2 需要依赖 request1 的请求结果
    }


  • 将相互依赖的请求在统一在内部处理

    • 例如,将 request2 放置到 request1 的具体实现中,这种方式适用于 request1request2 间在任何情况下都有紧密联系的情况下,当然也可以在 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.datadeepClone 深克隆 且修改后在往下层传递。


缺点很明显了:



  • 重复定义 props

    • 需要分别在 Grandpa、Parents、Son 三个组件中定义相关的 propsevent


  • 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 提供层 提供修改的方法。


946B61BF.gif


没有必要的响应式数据


砍树型写法


很多时候在 Vue 中我们需要在 模版中使用 常量数据,但并不会对其进行修改,如果我们没有在 Vue 组件实例上定义,那么在是无法访问到的,于是就是有如下用法:



<div class="app">
<ul>
<li v-for="num in constantData" :key="num">{{ num }}li>

ul>
div>


<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 能够被模版正常访问到即可,那么我们可以使用如下的写法:




原理也很简单,在 vue 的源码中有如下的判断:


image.png


即只要保证目标对象属性描述符的 configurable = false,就能够保证其不会被转成响应式数据,而这我们就可以使用 Object.freeze / Object.seal 来实现,如下:


image.png


image.png


易变的 key


砍树型写法


在需要实现通过 v-for 实现 列表渲染 时,大多数人喜欢直接使用 index 作为唯一 key,如下:


for="(item, index) in mockData" :key="index">


<button @click="deleteItem(index)">删除button>


特别是带有 删除/移动 操作的列表,使得 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 等来进行演示,如下:


  // 情况一
if="condition1 && condition2 && method3() && computed1">



// 情况二
<div v-show="condition1 && condition2 && method3() && computed1">

div>


// 情况三
<div :class="['class1', condition1 ? 'class2' : '', condition1 && condition2 ? 'class3' : '']">

div>


这种写法只会让这个条件判断写得越来越长,虽说支持在模版中写表达式,但是如此 长且复杂 的表达式直接写在 中属实不合适,应该要尽量精简,修改条件时也不应该跑到模版中去修改。

栽树型写法


在模版中的表达式要保持精简,换句话说就是把表达式的结果放在模版中就好了,那么可想而知就可以使用 computedmethod 了。


针对 v-ifv-show 可以使用如下方式:


image.png


针对 class 可以选择将 不变的可变的 进行 分开不分开 处理:


// 方式一:分开
class="class1"
:class="divConditionClass">



new Vue({
el: '#demo',
data: {},
computed: {
divConditionClass(){
return {
'class2' : condition1,
'class3': condition1 && condition2
};
},
}
})

// 方式二:不分开
class="divClass">



new Vue({
el: '#demo',
data: {},
computed: {
divClass(){
return {
'class1': true,
'class2' : condition1,
'class3': condition1 && condition2
};
},
}
})

v-if 和 v-for 共用


砍树型写法


v-if 和 v-for 一起使用已经老生常谈的问题了,但是还是会有人这样使用,如下:


image.png


先抛开 v-if 和 v-for 渲染优先级所带来的性能消耗不讲,单单是这个 红色波浪线 的提示难道还不够明显吗?


8FE6E7A4.jpg


不过值得注意的是,在 vue3v-if 和 v-for 同时使用时,v-if 的优先级会比 v-for 更高.


栽树型写法


v-if 和 v-for 无非就是为了控制 显示和隐藏,既然如此只要将不需要渲染的数据内容过滤出来即可,而这不就可以通过 computedmethod 来实现了,如下:


image.png


这里可能有人会说也可以使用 vue 提供的 filters 过滤器,但是过滤器只能用在两个地方:



双花括号插值 {{ }}
v-bind 表达式

也就是说如果你要在 v-for 中使用 过滤器 就会发生错误,如下:


// 错误的使用过滤器
for="(item, index) in mockData | filterData" :key="item.key">
{{ item.value }}


image.png


!important 重写样式


砍树型写法


当需要调整些样式问题时,发现改动无效,一排查发现 !important 卡得死死的,如下:


image.png


直接使用 !important 强制覆盖样式虽然方便,但是这卡得也太死了,而且还是不限制对应组件内容,直接全局覆盖,这样其他人想去改动估计都得掂量掂量,会不会影响某个页面的视图效果。


9478872A.jpg


栽树型写法


调整样式时先区分是 局部样式 还是 全局样式



如果是 局部样式 那么最好使用 scoped,或者是 页面顶层选择器 限定一下范围



如果是 全局样式 不要偷懒写在 当前组件 中,因为这样使得全局样式分离了,将来修改时也不好查找,如下:
// 注意没有 scoped





当然还有一种情况是 B 组件 的样式,在 A 组件 中需要调整,在其他地方使用 B 组件 时不需要调整,但此时如果直接在 A 组件scoped 样式中修改时可能不生效,这个时候需要在 scoped 限制外去调整 B 组件 的样式,那么可以这么写:


// A 组件中





单文件内容过多


砍树型写法


一个单文件的内容实在是太多、太长,如下:


image.png


太长的内容 出现问题不好排查、新增逻辑不好加,更何况是那些涉及到上下联动的逻辑。


那么是什么原因导致这么长呢?


因为 该抽离的内容没有抽离,例如:



模版的内容中 表单、表格、弹窗等内容没有很好抽离
部分

state 的初始化声明赋值等过长,例如与 表单、表格 相关的 state
相同的方法,总是要在不同的 .vue 文件重复声明



栽树型写法


那怎么抽离呢?


模版部分抽离



复杂或结构长的部分可以封装成组件

例如,表单、表格 的渲染就没必要一个结构一个结构的写,应该要通过 JSON Schema 的方式来实现,在 Vue 中正好可以配合 slot 插槽 实现自定义内容


复杂的弹窗内容也可以组件化

在上述的文件中,发现和弹窗相关的内容有 10 个,也就是写了 10 个 Dialog,如果后续还有其他不同的弹窗内容,结果可想而知,除此之外和 Dialog 相关的 state 是不是又得分别定义,因此十分不推荐这种方式
仔细想想,不同弹窗变的是弹窗内容和一些附带属性而已,因此 Dialog 可以只写一个,这意味着和 Dialog 相关的 state 也只需定义一个,针对不同的弹窗内容可以封装成组件,根据情况来渲染,也可以配合 实现



部分抽离



组件化

这一点不难理解,封装成组件后对应的逻辑也从当前组件中可以抽离出去


mixins 混入

虽说多个 mixins 会导致数据来源的不确定性,但不影响它做逻辑的抽离


自定义指令 directives

如果有一段逻辑是需要操作 真实 DOM,并且这段逻辑整体上是值得被复用的,那么可以将其封装成全局自定义指令


extend 继承

由于这个项目支持使用 class 的形式,那么一些 重复的属性、方法 就可以通过 extend 关键字来继承,无非就是定义某个公共的类,然后其他组件去继承这个类即可


抽离过长的 state 初始值

对于 state 的初始化赋值比较长的内容,可以将其抽离到相关独立文件中,简化主文件中不必要的代码长度,更清晰只观



最后



欢迎关注同名公众号《熊的猫》,文章会同步更新,也可快速加入前端交流群!



好了,暂时先写到这里吧!毕竟是写不完的!


948F627C.jpg


虽说做 Code Review 的时候,大家都说影响开发速度,毕竟开发时间限制在那,但是不做 Code Review 的时候,不同的开发者写法各式各样,重点是还能绕过规范检查工具,后期自己或他人维护时的难易程度可想而知,所以大家还是多多 “栽树” 吧!


希望本文对你有所帮助!!!


作者:熊的猫
来源:juejin.cn/post/7274163003158724608

0 个评论

要回复文章请先登录注册