尚品汇-性能优化


更新记录

  • 2023-09-13:对于“ v-if 中条件变化会引起哪些生命周期函数变化”作了补充,新增了 v-ifv-show 的原理实现。

v-if or v-show

在Vue.js中,v-ifv-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-iffalse变为true的时候,触发组件的beforeCreatecreatebeforeMountmounted钩子,由true变为false的时候触发组件的beforeDestorydestoryed方法

原理实现:

具体解析流程这里不展开讲,大致流程如下

  • 将模板template转为ast结构的JS对象
  • ast得到的JS对象拼装renderstaticRenderFns函数
  • renderstaticRenderFns函数被调用后生成虚拟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 秒内被重复触发,则重新计时

应用场景:

  1. 常见的应用场景包括搜索框输入联想。
  2. 窗口调整时的重新计算等。

简而言之:在固定的时间内,将多次操作变成一次操作,例如将多个请求合并为一次请求。

代码示例:

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 秒内重复触发,只有一次生效

简而言之:保证一段时间内只调用一次函数。

应用场景:

  1. 提交表单。
  2. 高频监听事件。

时间戳写法

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 },
 },

文章作者: QT-7274
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 QT-7274 !
评论
  目录