更新记录
- 2023-09-13:对于“
v-if
中条件变化会引起哪些生命周期函数变化”作了补充,新增了v-if
和v-show
的原理实现。
v-if
or v-show
?
在Vue.js中,v-if
和v-show
都是用于条件渲染的指令,它们的作用是根据条件来显示或隐藏元素。
v-if
是一种惰性渲染的方式,它会根据条件决定是否在DOM中创建或销毁元素。当条件为false
时,对应的元素将被完全从DOM中移除,而当条件为true
时,对应的元素将被重新创建并插入DOM中。这意味着当条件频繁变化时,v-if
会频繁地创建和销毁元素,对性能会有一定的影响。
v-show
则是通过CSS的display属性来控制元素的显示与隐藏。当条件为false
时,对应的元素会被隐藏(display: none),而当条件为true
时,对应的元素会被显示(display: 根据元素原有的display属性值来确定)。这意味着无论条件如何变化,元素始终保留在DOM中,只是通过CSS的控制来显示或隐藏,对性能影响较小。
v-if
在生成VNode
前就已经在模板编译阶段进行了判断,而v-show
也是在编译时解析,只不过在v-if
在编译时就确定了渲染元素,而v-show
在运行时根据条件进行显示和隐藏。
因此,尽可能使用v-show
而不是v-if
的原因是,当需要频繁切换显示与隐藏时,v-show
的性能更好,因为元素始终存在于DOM中,不需要频繁地创建和销毁。而v-if
适用于在条件较少变化或需要在条件为false
时彻底从DOM中移除元素的情况下使用。
注意:
v-show
只是通过CSS控制元素的显示与隐藏,并不会触发元素内部的生命周期钩子函数,而v-if
会在条件切换时触发元素的创建和销毁生命周期钩子函数。
v-if
由false
变为true
的时候,触发组件的beforeCreate
、create
、beforeMount
、mounted
钩子,由true
变为false
的时候触发组件的beforeDestory
、destoryed
方法
原理实现:
具体解析流程这里不展开讲,大致流程如下
- 将模板
template
转为ast
结构的JS
对象 - 用
ast
得到的JS
对象拼装render
和staticRenderFns
函数 render
和staticRenderFns
函数被调用后生成虚拟VNODE
节点,该节点包含创建DOM
节点所需信息vm.patch
函数通过虚拟DOM
算法利用VNODE
节点创建真实DOM
节点
v-show
原理
不管初始条件是什么,元素总是会被渲染
我们看一下在vue
中是如何实现的
代码很好理解,有transition
就执行transition
,没有就直接设置display
属性
// https://github.com/vuejs/vue-next/blob/3cd30c5245da0733f9eb6f29d220f39c46518162/packages/runtime-dom/src/directives/vShow.ts
export const vShow: ObjectDirective<VShowElement> = {
beforeMount(el, { value }, { transition }) {
el._vod = el.style.display === 'none' ? '' : el.style.display
if (transition && value) {
transition.beforeEnter(el)
} else {
setDisplay(el, value)
}
},
mounted(el, { value }, { transition }) {
if (transition && value) {
transition.enter(el)
}
},
updated(el, { value, oldValue }, { transition }) {
// ...
},
beforeUnmount(el, { value }) {
setDisplay(el, value)
}
}
v-if
原理
v-if
在实现上比v-show
要复杂的多,因为还有else
else-if
等条件需要处理,这里我们也只摘抄源码中处理 v-if
的一小部分
返回一个node
节点,render
函数通过表达式的值来决定是否生成DOM
// https://github.com/vuejs/vue-next/blob/cdc9f336fd/packages/compiler-core/src/transforms/vIf.ts
export const transformIf = createStructuralDirectiveTransform(
/^(if|else|else-if)$/,
(node, dir, context) => {
return processIf(node, dir, context, (ifNode, branch, isRoot) => {
// ...
return () => {
if (isRoot) {
ifNode.codegenNode = createCodegenNodeForBranch(
branch,
key,
context
) as IfConditionalExpression
} else {
// attach this branch's codegen node to the v-if root.
const parentCondition = getParentCondition(ifNode.codegenNode!)
parentCondition.alternate = createCodegenNodeForBranch(
branch,
key + ifNode.branches.length - 1,
context
)
}
}
})
}
)
防抖(Debounce)
防抖是指在事件触发后,等待一段时间后再执行回调函数。如果在等待时间内再次触发了该事件,则重新计时。防抖的主要目的是减少事件触发的频率,避免在短时间内多次触发事件导致不必要的计算或请求。
n 秒后在执行该事件,若在 n 秒内被重复触发,则重新计时
应用场景:
- 常见的应用场景包括搜索框输入联想。
- 窗口调整时的重新计算等。
简而言之:在固定的时间内,将多次操作变成一次操作,例如将多个请求合并为一次请求。
代码示例:
function antiShake(fn, wait){
let timeout = null;
return args => {
if(timeout) {clearTimeout(timeout)}
timeout = setTimeout(fn,wait)
}
}
function demo(){
console.log('发起请求')
}
如果需要立即执行:
function debounce(func, wait, immediate) {
let timeout;
return function () {
let context = this;
let args = arguments;
if (timeout) clearTimeout(timeout); // timeout 不为null
if (immediate) {
let callNow = !timeout; // 第一次会立即执行,以后只有事件执行后才会再次触发
timeout = setTimeout(function () {
timeout = null;
}, wait)
if (callNow) {
func.apply(context, args)
}
}
else {
timeout = setTimeout(function () {
func.apply(context, args)
}, wait);
}
}
}
节流(Throttle)
节流是指在一定时间间隔内只执行一次回调函数。当事件触发后,如果在指定的时间间隔内再次触发该事件,则忽略该事件,直到时间间隔过去后才能再次触发。节流的主要目的是控制事件触发的频率,避免在短时间内连续触发事件导致性能问题。
n 秒内只运行一次,若在 n 秒内重复触发,只有一次生效
简而言之:保证一段时间内只调用一次函数。
应用场景:
- 提交表单。
- 高频监听事件。
时间戳写法
function throttled1(fn, delay = 500) {
let oldtime = Date.now()
return function (...args) {
let newtime = Date.now()
if (newtime - oldtime >= delay) {
fn.apply(null, args)
oldtime = Date.now()
}
}
}
缺点:使用时间戳写法,事件会立即执行,停止触发后没有办法再次执行
定时器写法
function throttle(event, time){
let timer = null;
return function(){
if(!timer){
timer = setTimeout(()=>{
event()
timer = null
}, time)
}
}
}
缺点:使用定时器写法,delay
毫秒后第一次执行,第二次事件停止触发后依然会再一次执行
精确节流
可以将时间戳写法的特性与定时器写法的特性相结合,实现一个更加精确的节流。实现如下
代码示例:
function throttled(fn, delay) {
let timer = null
let starttime = Date.now()
return function () {
let curTime = Date.now() // 当前时间
let remaining = delay - (curTime - starttime) // 从上一次到现在,还剩下多少多余时间
let context = this
let args = arguments
clearTimeout(timer)
if (remaining <= 0) {
fn.apply(context, args)
starttime = Date.now()
} else {
timer = setTimeout(fn, remaining);
}
}
}
总结
- 如果需要在事件触发后立即执行回调函数,并且不希望在短时间内连续触发事件,则可以使用防抖。
- 如果需要在一定时间间隔内稳定地执行回调函数,并且不希望事件频繁触发,则可以使用节流。
//按需引入lodash节流函数
import throttle from "lodash/throttle";
//按需引入lodash防抖函数
import debounce from "lodash/debounce";
//修改商品数据->加的操作,节流防止用户频繁点击,置于按钮
addSkuNum: throttle(async function(cart) {
//整理参数
let params = { skuId: cart.skuId, skuNum: 1 };
//发请求:通知服务器修改当前商品的个数,详见接口文档,接收正负数来判断是加还是减
//再次获取购物车的最新的数据:保证这次修改数据完毕【成功以后在获取购物车数据】
try {
//修改商品个数成功
await this.$store.dispatch("addOrUpdateCart", params);
//再次获取最新的购物车的数据
this.getData();
} catch (error) {
alert("修改数量失败");
}
},2000),
//防抖防止用户频繁输入
changeSkuNum: debounce(async function (cart, e) {
//整理参数
let params = { skuId: cart.skuId };
//使用正则表达式使得用户输入的内容只能是大于0的整数
let reg = /^[1-9]\d*$/;
if (!reg.test(e.target.value)) {
//将用户的非法输入的内容置为0,而不是全部清空
params.skuNum = 0;
} else {
let userResultValue = e.target.value * 1;
params.skuNum = parseInt(userResultValue) - cart.skuNum;
}
//发请求:修改商品的个数
try {
//修改商品的个数、成功以后再次获取购物车的数据
await this.$store.dispatch("addOrUpdateCart", params);
this.getData();
} catch (error) {}
}, 500),
路由懒加载
{
path: "/trade",
component: () => import("@/views/Trade/index.vue"),
meta: { isHideFooter: true },
},
{
path: "/pay",
component:() => import("@/views/Pay/index.vue"),
meta: { isHideFooter: true },
},