温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

怎么使用v-lazy-show编译模板指令

发布时间:2023-04-17 15:42:45 来源:亿速云 阅读:153 作者:iii 栏目:开发技术

这篇文章主要讲解了“怎么使用v-lazy-show编译模板指令”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“怎么使用v-lazy-show编译模板指令”吧!

    前言

    简单的说,v-lazy-show 是一个编译时指令,就是对 v-show 的一种优化,因为我们知道,v-show 的原理只是基于简单的切换 display none,false则为none,true则移除

    但即使在第一次条件为 falsy 的时候,其依然会渲染对应的组件,那如果该组件很大,就会带来额外的渲染开销,比如我们有个 Tabs,默认初始显示第一个 tab,但后面的 tab 也都渲染了,只是没有显示罢了(实际上没有必要,因为可能你点都不会点开)。

    那基于此种情况下,我们可以优化一下,即第一次条件为 falsy 的情况下,不渲染对应的组件,直到条件为 truthy 才渲染该组件。

    将原本的 v-show 改为 v-lazy-show 或者 v-show.lazy

    <script setup lang="ts">
    import { ref } from 'vue'
    import ExpansiveComponent from './ExpansiveComponent.vue'
    class="brush:js;"const enabled = ref(false)
    </script>
    class="brush:js;"<template>
      <button @click="enabled = !enabled">
        Toggle
      </button>
    class="brush:js;"  <div class="hello-word-wrapper">
        <ExpansiveComponent v-lazy-show="enabled" msg="v-lazy-show" />
        <ExpansiveComponent v-show.lazy="enabled" msg="v-lazy.show" />
    class="brush:js;"    <ExpansiveComponent v-show="enabled" msg="v-show" />
    class="brush:js;"    <ExpansiveComponent v-if="enabled" msg="v-if" />
      </div>
    </template>
    <!-- ExpansiveComponent.vue -->
    <script setup lang="ts">
    import { onMounted } from 'vue'
    class="brush:js;"const props = defineProps({
      msg: {
        type: String,
        required: true,
      },
    })
    class="brush:js;"onMounted(() => {
      console.log(`${props.msg} mounted`)
    })
    </script>
    class="brush:js;"<template>
      <div>
        <div v-for="i in 1000" :key="i">
          Hello {{ msg }}
        </div>
      </div>
    </template>

    ExpansiveComponent 渲染了 1000 行 div,在条件 enabled 初始为 false 的情况下,对应 v-show 来说,其依然会渲染,而对于 v-lazy-show 或 v-show.lazy 来说,只有第一次 enabled 为 true 才渲染,避免了不必要的初始渲染开销

    如何使用?

    国际惯例,先装下依赖,这里强烈推荐 antfu 大佬的 ni

    npm install v-lazy-show -D
    yarn add v-lazy-show -D
    pnpm add v-lazy-show -D
    ni v-lazy-show -D

    既然是个编译时指令,且是处理 vue template 的,那么就应该在对应的构建工具中配置,如下:

    如果你用的是 vite,那么配置如下

    // vite.config.ts
    import { defineConfig } from 'vite'
    import { transformLazyShow } from 'v-lazy-show'
    class="brush:js;"export default defineConfig({
      plugins: [
        Vue({
          template: {
            compilerOptions: {
              nodeTransforms: [
                transformLazyShow, // <--- 加在这里
              ],
            },
          },
        }),
      ]
    })

    如果你用的是 Nuxt,那么应该这样配置:

    // nuxt.config.ts
    import { transformLazyShow } from 'v-lazy-show'
    class="brush:js;"export default defineNuxtConfig({
      vue: {
        compilerOptions: {
          nodeTransforms: [
            transformLazyShow, // <--- 加上这行
          ],
        },
      },
    })

    那么,该指令是如何起作用的?

    上面的指令作用很好理解,那么其是如何实现的呢?我们看下大佬是怎么做的。具体可见源码

    源码不多,我这里直接贴出来,再一步步看如何实现(这里快速过一下即可,后面会一步步分析):

    import {
      CREATE_COMMENT,
      FRAGMENT,
      createCallExpression,
      createCompoundExpression,
      createConditionalExpression,
      createSequenceExpression,
      createSimpleExpression,
      createStructuralDirectiveTransform,
      createVNodeCall,
      traverseNode,
    } from '@vue/compiler-core'
    class="brush:js;"const indexMap = new WeakMap()
    class="brush:js;"// https://github.com/vuejs/core/blob/f5971468e53683d8a54d9cd11f73d0b95c0e0fb7/packages/compiler-core/src/ast.ts#L28
    const NodeTypes = {
      SIMPLE_EXPRESSION: 4,
    }
    class="brush:js;"// https://github.com/vuejs/core/blob/f5971468e53683d8a54d9cd11f73d0b95c0e0fb7/packages/compiler-core/src/ast.ts#L62
    const ElementTypes = {
      TEMPLATE: 3,
    }
    class="brush:js;"// https://github.com/vuejs/core/blob/f5971468e53683d8a54d9cd11f73d0b95c0e0fb7/packages/shared/src/patchFlags.ts#L19
    const PatchFlags = {
      STABLE_FRAGMENT: 64,
    }
    class="brush:js;"export const transformLazyShow = createStructuralDirectiveTransform(
      /^(lazy-show|show)$/,
      (node, dir, context) => {
        // forward normal `v-show` as-is
        if (dir.name === 'show' && !dir.modifiers.includes('lazy')) {
          return () => {
            node.props.push(dir)
          }
        }
    class="brush:js;"    const directiveName = dir.name === 'show'
          ? 'v-show.lazy'
          : 'v-lazy-show'
    class="brush:js;"    if (node.tagType === ElementTypes.TEMPLATE || node.tag === 'template')
          throw new Error(`${directiveName} can not be used on <template>`)
    class="brush:js;"    if (context.ssr || context.inSSR) {
          // rename `v-lazy-show` to `v-if` in SSR, and let Vue handles it
          node.props.push({
            ...dir,
            exp: dir.exp
              ? createSimpleExpression(dir.exp.loc.source)
              : undefined,
            modifiers: dir.modifiers.filter(i => i !== 'lazy'),
            name: 'if',
          })
          return
        }
    class="brush:js;"    const { helper } = context
        const keyIndex = (indexMap.get(context.root) || 0) + 1
        indexMap.set(context.root, keyIndex)
    class="brush:js;"    const key = `_lazyshow${keyIndex}`
    class="brush:js;"    const body = createVNodeCall(
          context,
          helper(FRAGMENT),
          undefined,
          [node],
          PatchFlags.STABLE_FRAGMENT.toString(),
          undefined,
          undefined,
          true,
          false,
          false /* isComponent */,
          node.loc,
        )
    class="brush:js;"    const wrapNode = createConditionalExpression(
          createCompoundExpression([`_cache.${key}`, ' || ', dir.exp!]),
          createSequenceExpression([
            createCompoundExpression([`_cache.${key} = true`]),
            body,
          ]),
          createCallExpression(helper(CREATE_COMMENT), [
            '"v-show-if"',
            'true',
          ]),
        ) as any
    class="brush:js;"    context.replaceNode(wrapNode)
    class="brush:js;"    return () => {
          if (!node.codegenNode)
            traverseNode(node, context)
    class="brush:js;"      // rename `v-lazy-show` to `v-show` and let Vue handles it
          node.props.push({
            ...dir,
            modifiers: dir.modifiers.filter(i => i !== 'lazy'),
            name: 'show',
          })
        }
      },
    )

    createStructuralDirectiveTransform

    因为是处理运行时的指令,那么自然用到了 createStructuralDirectiveTransform 这个函数,我们先简单看下其作用:

    createStructuralDirectiveTransform 是一个工厂函数,用于创建一个自定义的 transform 函数,用于在编译过程中处理特定的结构性指令(例如 v-for, v-if, v-else-if, v-else 等)。

    该函数有两个参数:

    nameMatcher:一个正则表达式或字符串,用于匹配需要被处理的指令名称。

    fn:一个函数,用于处理结构性指令。该函数有三个参数:

    • node:当前节点对象。

    • dir:当前节点上的指令对象。

    • context:编译上下文对象,包含编译期间的各种配置和数据。

    createStructuralDirectiveTransform 函数会返回一个函数,该函数接收一个节点对象和编译上下文对象,用于根据指定的 nameMatcher 匹配到对应的指令后,调用用户自定义的 fn 函数进行处理。

    在编译过程中,当遇到符合 nameMatcher 的结构性指令时,就会调用返回的处理函数进行处理,例如在本例中,当遇到 v-show 或 v-lazy-show 时,就会调用 transformLazyShow 处理函数进行处理。

    不处理 v-show

    if (dir.name === 'show' && !dir.modifiers.includes('lazy')) {
        return () => {
          node.props.push(dir)
        }
      }

    因为 v-show.lazy 是可以生效的,所以 v-show 会进入该方法,但如果仅仅只是 v-show,而没有 lazy 修饰符,那么实际上不用处理

    这里有个细节,为何要将指令对象 push 进 props,不 push 行不行?

    原先的表现是 v-show 条件为 false 时 display 为 none,渲染了节点,只是不显示:

    怎么使用v-lazy-show编译模板指令

    而注释node.props.push(dir)后,看看页面表现咋样:

    怎么使用v-lazy-show编译模板指令

    v-show 的功能没了,也就是说指令的功能会添加到 props 上,所以这里要特别注意,不是单纯的返回 node 即可。后来还有几处node.props.push,原理跟这里一样。

    服务端渲染目前是转为 v-if

    if (context.ssr || context.inSSR) {
          // rename `v-lazy-show` to `v-if` in SSR, and let Vue handles it
      node.props.push({
        ...dir,
        exp: dir.exp
          ? createSimpleExpression(dir.exp.loc.source)
          : undefined,
        modifiers: dir.modifiers.filter(i => i !== 'lazy'),
        name: 'if',
      })
      return
    }

    将 v-lazy-show 改名为 v-if,且过滤掉修饰符

    createVNodeCall 给原先节点包一层 template

    顾名思义,createVNodeCall 是 用来创建一个 vnode 节点的函数:

    const body = createVNodeCall(
          /** 当前的上下文 (context) 对象,即 CodegenContext */
          context,
          /** helper 函数是 Vue 内部使用的帮助函数。FRAGMENT 表示创建 Fragment 节点的 helper 函数 */
          helper(FRAGMENT),
          /** 组件的 props */
          undefined,
          /** 当前节点的子节点数组,即包含有指令的节点本身 */
          [node],
          /** 表示该节点的 PatchFlag,指明了该节点是否稳定、是否具有一些特定的更新行为等。STABLE_FRAGMENT 表示该 Fragment 节点是一个稳定的节点,即其子节点不会发生改变 */
          PatchFlags.STABLE_FRAGMENT.toString(),
          /** 该节点的动态 keys */
          undefined,
          /** 该节点的模板引用 (ref) */
          undefined,
          /** 表示该节点是否需要开启 Block (块) 模式,即是否需要对其子节点进行优化 */
          true,
          /** 表示该节点是否是一个 Portal 节点 */
          false,
          /** 表示该节点是否是一个组件 */
          false /* isComponent */,
          /** 该节点在模板中的位置信息 */
          node.loc,
    )

    参数含义如下,简单了解即可(反正看了就忘)

    也就是说,其会生成如下模板:

    <template>
      <ExpansiveComponent v-lazy-show="enabled" msg="v-lazy-show" />
    </template>

    关键代码(重点)

    接下来这部分是主要原理,请打起十二分精神。

    先在全局维护一个 map,代码中叫 indexMap,是一个 WeakMap(不知道 WeakMap 的可以去了解下)。然后为每一个带有 v-lazy-show 指令的生成一个唯一 key,这里叫做_lazyshow${keyIndex},也就是第一个就是_lazyshow1,第二个是_lazyshow2...

      const keyIndex = (indexMap.get(context.root) || 0) + 1
      indexMap.set(context.root, keyIndex)
    class="brush:js;"  const key = `_lazyshow${keyIndex}`

    然后将生成的key放到渲染函数的_cache上(渲染函数的第二个参数,function render(_ctx, _cache)),即通过_cache.${key}作为辅助变量。之后会根据 createConditionalExpression 创建一个条件表达式

    const wrapNode = createConditionalExpression(
          createCompoundExpression([`_cache.${key}`, ' || ', dir.exp!]),
          createSequenceExpression([
            createCompoundExpression([`_cache.${key} = true`]),
            body,
          ]),
          // 生成一个注释节点 `<!--v-show-if-->`
          createCallExpression(helper(CREATE_COMMENT), [
            '"v-show-if"',
            'true',
          ]),
    )

    也就是说, v-lazy-show 初始传入的条件为 false 时,那么会为你创建一个注释节点,用来占位:

    createCallExpression(helper(CREATE_COMMENT), [
      '"v-show-if"',
      'true',
    ])

    怎么使用v-lazy-show编译模板指令

    这个跟 v-if 一样

    直到第一次条件为真时,将 _cache.${key} 置为 true,那么以后的行为就跟 v-show 一致了,上面的 dir.exp 即指令中的条件,如

    <div v-show="enabled"/>

    enabled 即 exp,表达式的意思。

    readme给出的转换如下:

    <template>
      <div v-lazy-show="foo">
        Hello
      </div>
    </template>

    会转换为:

    import { Fragment as _Fragment, createCommentVNode as _createCommentVNode, createElementBlock as _createElementBlock, createElementVNode as _createElementVNode, openBlock as _openBlock, vShow as _vShow, withDirectives as _withDirectives } from 'vue'
    class="brush:js;"export function render(_ctx, _cache) {
      return (_cache._lazyshow1 || _ctx.foo)
        ? (_cache._lazyshow1 = true, (_openBlock(),
          _withDirectives(_createElementVNode('div', null, ' Hello ', 512 /* NEED_PATCH */), [
            [_vShow, _ctx.foo]
          ])))
        : _createCommentVNode('v-show-if', true)
    }

    你可以简单理解为会将<ExpansiveComponent msg="v-lazy-show" v-lazy-show=""enabled"/>转为下面:

    <template v-if="_cache._lazyshow1 || enabled">
        <!-- 为true时会把_cache._lazyshow1置为true,那么以后的v-if就用于为true了 -->
        <ExpansiveComponent msg="v-lazy-show" v-lazy-show="enabled"/>
    </template>
    <template v-else>
      <!--v-show-if-->
    </template>
    class="brush:js;"<template v-if="_cache._lazyshow2 || enabled">
        <!-- 为true时会把_cache._lazyshow2置为true,那么以后的v-if就用于为true了 -->
        <ExpansiveComponent msg="v-lazy-show" v-show.lazy="enabled"/>
    </template>
    <template v-else>
      <!--v-show-if-->
    </template>

    然后将原先节点替换为处理后的 wrapperNode 即可

    context.replaceNode(wrapNode)

    最后将 v-lazy-show | v-shouw.lazy 处理为 v-show

    因为 vue 本身是没有 v-lazy-show 的,v-show 也没有 lazy 的的修饰符,那么要让指令生效,就要做到两个:

    • 将原先的 show-lazy 改名为 show

    • 过滤掉 lazy 的修饰符

    node.props.push({
       ...dir,
       modifiers: dir.modifiers.filter(i => i !== 'lazy'),
       name: 'show',
     })

    也就变成这样啦:

    <template v-if="_cache._lazyshow1 || enabled">
        <!-- 为true时会把_cache._lazyshow1置为true,那么以后的v-if就用于为true了 -->
        <ExpansiveComponent msg="v-lazy-show" v-show="enabled"/>
    </template>
    <template v-else>
      <!--v-show-if-->
    </template>
    <template v-if="_cache._lazyshow2 || enabled">
        <!-- 为true时会把_cache._lazyshow2置为true,那么以后的v-if就用于为true了 -->
        <ExpansiveComponent msg="v-show.lazy" v-show="enabled"/>
    </template>
    <template v-else>
      <!--v-show-if-->
    </template>

    小结一下:

    为每一个使用 v-lazy-show 分配唯一的 key,放到渲染函数内部的_cache上,即借助辅助变量_cache.${key}

    • 当初始条件为 falsy 时不渲染节点,只渲染注释节点 <!--v-show-if-->

    • 直到条件为真时将其置为 true,之后的表现就跟 v-show 一致了

    • 由于 vue 不认识 v-lazy-show,v-show.lazy,使用要将指令改回 v-show,且过滤掉 lazy 修饰符(如果使用 v-show.lazy 的话)

    感谢各位的阅读,以上就是“怎么使用v-lazy-show编译模板指令”的内容了,经过本文的学习后,相信大家对怎么使用v-lazy-show编译模板指令这一问题有了更深刻的体会,具体使用情况还需要大家实践验证。这里是亿速云,小编将为大家推送更多相关知识点的文章,欢迎关注!

    向AI问一下细节

    免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

    AI