Products
GG网络技术分享 2025-03-18 16:14 0
使用 v-for 更新已渲染的元素列表时,默认用就地复用策略;列表数据修改的时候,他会根据 key 值去判断某个值是否修改,如果修改,则重新渲染这一项,否则复用之前的元素;
使用key的注意事项:
这是一个非常有考究的问题,首先你要知道 vue 中的 原地复用 (大概就是 虚拟dom 变化时,两个 虚拟dom节点 的 key 如果一样就不会重新创建节点,而是修改原来的节点)
当我们渲染的数据不需要保持状态时,例如常见的单纯的表格分页渲染(不包含输入,只是展示)、下拉加载更多等场景,那么使用 index 作为 key 再好不过,因为进入下一页或者上一页时就会原地复用之前的节点,而不是重新创建,如果使用唯一的 id 作为 key 反而会重新创建dom,性能相对较低。
此外使用 index 作为 key 我还应该要尽量避免对数组的中间进行 增加/删除 等会影响后面元素key变化的操作。这会让 vue 认为后面所有元素都发生了变化,导致多余的对比和原地复用。
所以使用 index 作为 key 需要满足:
对于大多数数据的 id 都是唯一的,这无疑的一个 key 的优选答案。对于任何大多数情况使用 id 作为 key 都不会出现上面 bug。但是如果你需要考虑性能问题,那就就要思考是否应该使用原地复用了。
同样是上面的分页数据展示,如果使用 id 作为 key ,可想而知每一页的每一条数据 id 都是不一样的,所以当换页时两颗 虚拟DOM树 的节点的 key 完全不一致,vue 就会移除原来的节点然后创建新的节点。可想而知效率会更加低下。但是他也有它的优点。唯一的 key 可以帮助 diff 更加精确的为我们绑定状态,这尤其适合数据有独立的状态的场景,例如带输入框或者单选框的列表数据。
所以何时使用 id 作为 key?只有一点:
原因:默认情况下,Vue 会尽可能高效的更新 DOM。这意味着其在相同类型的元素之间切换时,会修补已存在的元素,而不是将旧的元素移除然后在同一位置添加一个新元素。如果本不相同的元素被识别为相同,则会出现意料之外的副作用。
相对于 v-for 的 key, v-if/v-else-if/v-else 中的 key 相对简单,我们可以直接写入固定的字符串或者数组即可
<transition>
<button
v-if="isEditing"
v-on:click="isEditing = false"
>
Save
</button>
<button
v-else
v-on:click="isEditing = true"
>
Edit
</button>
</transition>
.v-enter-active, .v-leave-active {
transition: all 1s;
}
.v-enter, .v-leave-to {
opacity: 0;
transform: translateY(30px);
}
.v-leave-active {
position: absolute;
}
例如对于上面的代码, 你会发现虽然对 button 添加了 过渡效果, 但是如果不添加 key 切换时是无法触发过渡的
这个大家都知道
原因是 v-for 的 优先级高于 v-if,所以当它们使用再同一个标签上是,每一个渲染都会先循环再进行条件判断
例如下面这段代码在 Vue2 中是不被推荐的,Vue 也会给出对应的警告
<ul>
<li v-for="user in users" v-if="user.active">
{{ user.name }}
</li>
</ul>
我们应该尽量将 v-if 移动到上级 或者 使用 计算属性来处理数据
<ul v-if="active">
<li v-for="user in users">
{{ user.name }}
</li>
</ul>
如果你不想让循环的内容多出一个无需有的上级容器,那么你可以选择使用 template 来作为其父元素,template 不会被浏览器渲染为 DOM 节点
如果我想要判断遍历对象里面每一项的内容来选择渲染的数据的话,可以使用 computed 来对遍历对象进行过滤
// js
let usersActive = computed(()=>users.filter(user => user.active))
// template
<ul>
<li v-for="user in usersActive">
{{ user.name }}
</li>
</ul>
v-if 和 v-show 的区别相比大家都非常熟悉了;v-if 通过直接操作 DOM 的删除和添加来控制元素的显示和隐藏;v-show 是通过控制 DOM 的 display CSS熟悉来控制元素的显示和隐藏
由于对 DOM 的 添加/删除 操作性能远远低于操作 DOM 的 CSS 属性
所以当元素需要频繁的 显示/隐藏 变化时,我们使用 v-show 来提高性能。
当元素不需要频繁的 显示/隐藏 变化时,我们通过 v-if 来移除 DOM 可以节约掉浏览器渲染这个的一部分DOM需要的资源
computed 大家后很熟悉, 它会在其表达式中依赖的响应式数据发送变化时重新计算。如果我们在一个计算属性中书写了比较复杂的表达式,那么其依赖的响应式数据也任意变得更多。当其中任何一个依赖项变化时整个表达式都需要重新计算
let price = computed(()=>{
let basePrice = manufactureCost / (1 - profitMargin)
return (
basePrice -
basePrice * (discountPercent || 0)
)
})
当 manufactureCost、profitMargin、discountPercent 中任何一个变化时都会重新计算整个 price。
但是如果我们改成下面这样
let basePrice = computed(() => manufactureCost / (1 - profitMargin))
let discount = computed(() => basePrice * (discountPercent || 0))
let finalPrice = computed(() => basePrice - discount)
如果当 discountPercent 变化时,只会 重新计算 discount 和 finalPrice,由于 computed 的缓存特性,不会重新计算 basePrice
优化前
<template>
<div class="cell">
<div v-if="value" class="on"></div>
<section v-else class="off"></section>
</div>
</template>
<script>
export default {
props: ['value'],
}
</script>
优化后
<template functional>
<div class="cell">
<div v-if="props.value" class="on"></div>
<section v-else class="off"></section>
</div>
</template>
<script>
export default {
props: ['value'],
}
</script>
什么?你写的一个vue文件有一千多行代码?
合理的拆分组件不仅仅可以优化性能,还能够让代码更清晰可读。单一功能原则嘛
源自 slides.com/akryum/vuec…[3]
优化前
<template>
<div :style="{ opacity: number / 300 }">
<div>{{ heavy() }}</div>
</div>
</template>
<script>
export default {
props: ['number'],
methods: {
heavy () { /* HEAVY TASK */ }
}
}
</script>
优化后
<template>
<div :style="{ opacity: number / 300 }">
<ChildComp/>
</div>
</template>
<script>
export default {
props: ['number'],
components: {
ChildComp: {
methods: {
heavy () { /* HEAVY TASK */ }
},
render (h) {
return h('div', this.heavy())
}
}
}
}
</script>
由于 Vue 的更新是组件粒度的,虽然每一帧都通过数据修改导致了父组件的重新渲染,但是 ChildComp 却不会重新渲染,因为它的内部也没有任何响应式数据的变化。所以优化后的组件不会在每次渲染都执行耗时任务
优化前
<template>
<div :style="{ opacity: start / 300 }">{{ result }}</div>
</template>
<script>
import { heavy } from '@/utils'
export default {
props: ['start'],
computed: {
base () { return 42 },
result () {
let result = this.start
for (let i = 0; i < 1000; i++) {
result += heavy(this.base)
}
return result
}
}
}
</script>
优化后
<template>
<div :style="{ opacity: start / 300 }">
{{ result }}</div>
</template>
<script>
import { heavy } from '@/utils'
export default {
props: ['start'],
computed: {
base () { return 42 },
result () {
const base = this.base
let result = this.start
for (let i = 0; i < 1000; i++) {
result += heavy(base)
}
return result
}
}
}
</script>
在一些渲染成本比较高的组件需要被经常切换时,可以使用 keep-alive 来缓存这个组件
而在使用 keep-alive 后,被 keep-alive 包裹的组件在经过第一次渲染后,的 vnode 以及 DOM 都会被缓存起来,然后再下一次再次渲染该组件的时候,直接从缓存中拿到对应的 vnode 和 DOM,然后渲染,并不需要再走一次组件初始化,render 和 patch 等一系列流程,减少了 script 的执行时间,性能更好。
当一个组件被销毁时,我们应该清除组件中添加的 全局事件 和 定时器 等来防止内存泄漏
Vue3 的 HOOK 可以让我们将事件的声明和销毁写在一起,更加可读
function scrollFun(){ /* ... */}
document.addEventListener("scroll", scrollFun)
onBeforeUnmount(()=>{
document.removeEventListener("scroll", scrollFun)
})
Vue2 依然可以通过 $once 来做到这样的效果,当然你也可以在 optionsAPI beforeDestroy 中销毁事件,但是我更加推荐前者的写法,因为后者会让相同功能的代码更分散
function scrollFun(){ /* ... */}
document.addEventListener("scroll", scrollFun)
this.$once('hook:beforeDestroy', ()=>{
document.removeEventListener("scroll", scrollFun)
})
function scrollFun(){ /* ... */}
export default {
created() {
document.addEventListener("scroll", scrollFun)
},
beforeDestroy(){
document.removeEventListener("scroll", scrollFun)
}
}
图片懒加载:适用于页面上有较多图片且并不是所有图片都在一屏中展示的情况,vue-lazyload 插件给我们提供了一个很方便的图片懒加载指令 v-lazy
但是并不是所有图片都适合使用懒加载,例如 banner、相册等 更加推荐使用图片预加载技术,将当前展示图片的前一张和后一张优先下载。
这个相对比较考验 数据结构和算法 的功底
例如一个将数组转化为多级结构的方法
/**
* 数组转树形结构,时间复杂度O(n)
* @param list 数组
* @param idKey 元素id键
* @param parIdKey 元素父id键
* @param parId 第一级根节点的父id值
* @return {[]}
*/
function listToTree (list,idKey,parIdKey,parId) {
let map = {};
let result = [];
let len = list.length;
// 构建map
for (let i = 0; i < len; i++) {
//将数组中数据转为键值对结构 (这里的数组和obj会相互引用,这是算法实现的重点)
map[list[i][idKey]] = list[i];
}
// 构建树形数组
for(let i=0; i < len; i++) {
let itemParId = list[i][parIdKey];
// 顶级节点
if(itemParId === parId) {
result.push(list[i]);
continue;
}
// 孤儿节点,舍弃(不存在其父节点)
if(!map[itemParId]){
continue;
}
// 将当前节点插入到父节点的children中(由于是引用数据类型,obj中对于节点变化,result中对应节点会跟着变化)
if(map[itemParId].children) {
map[itemParId].children.push(list[i]);
} else {
map[itemParId].children = [list[i]];
}
}
return result;
}
除了上面说的方法以外还有很多优化技巧,只是我在项目并不是太常用
我在项目中关于首屏优化主要有以下几个优化方向
代码分割的作用的将打包产物分割为一个一个的小产物,其依赖 esModule。所以当你使用 import() 函数来导入一个文件或者依赖,那么这个文件或者依赖就会被单独打包为一个小产物。路由懒加载 和 异步组件 都是使用这个原理。
对于 UI库 我一般不会使用按需加载组件,而是比较喜欢 CDN 引入的方式来优化。
CDN: 首先就是上面的说的 CDN 引入把,开发阶段使用本地库,通过配置 外部扩展(Externals) 打包时来排除这些依赖。然后在 html 文件中通过 CDN 的方式来引入它们
Server Push: HTTP2已经相对成熟了;经过上面的 CDN 引入,我们可以对网站使用 HTTP2 的 Server Push 功能来让浏览器提前加载 这些 CDN 和 其他文件。
开启 gzip: 这个上面已经说过了,其原理就是当客户端和服务端都支持 gzip 传输时,服务端会优先发送经过 gzip 压缩过的文件,然后客户端接收到在进行解压。
开启缓存: 一般我使用的是协商缓存,但是这并不适用于所有情况,例如对于使用了 Server Push 的文件,就不能随意的修改其文件名。所以我一般还会将生产的主要文件固定文件名
因为项目中有个填写信息的页面,有很多input框,引入vue-scroller后发现在input区域滑动失效;看了一下引入的vue-scroller组件里的源码,发现在组件源码里的Scroller.vue中在touchStart、touchMove和mouseDownd方法的时候被return了
touchStart(e) { // Don\'t react if initial down happens on a form element //注释掉这段代码就行 // if (e.target.tagName.match(/input|textarea|select/i)) { // return // } //注释掉这段代码就行 if (e.target.tagName.match(/textarea|select/i)) { return; } this.scroller.doTouchStart(e.touches, e.timeStamp); }, touchMove(e) { //touchMove也要重新加个判断,否则input框不能左右滑动 if (!e.target.tagName.match(/input/i)) { e.preventDefault(); } this.scroller.doTouchMove(e.touches, e.timeStamp); }, mouseDown(e) { // Don\'t react if initial down happens on a form element //注释掉这段代码就行 // if (e.target.tagName.match(/input|textarea|select/i)) { // return // } //注释掉这段代码就行 if (e.target.tagName.match(/textarea|select/i)) { return; } this.scroller.doTouchStart( [ { pageX: e.pageX, pageY: e.pageY } ], e.timeStamp ); this.mousedown = true; }, |
具体我也不知道阻止滑动input框区域是为了防止什么Bug,有知道的可以在评论区说一下;最后因为vue-scroller是npm引入的方式,怕会被更新掉,所以直接把组件代码copy出来放在项目的components目录下引用了
main.js引入方式和插件类似,使用方式和官方一样
到此这篇关于使用Vue-scroller页面input框不能触发滑动的问题及解决方法的文章就介绍到这了,希望大家以后多多支持!
Demand feedback