渐进式JavaScript框架:Vue 过渡 & 动画 & 可复用性 & 组合

渐进式JavaScript框架:Vue 过渡 & 动画 & 可复用性 & 组合

前言

Vue2在过渡动画、组件可复用性和代码组合方面提供了灵活且强大的解决方案,既满足简单场景的快速实现,也支持复杂项目的工程化需求。

过渡 & 动画

Vue 中,进入 / 离开过渡和列表过渡是处理元素动态变化时视觉效果的两种核心场景。它们分别针对单个元素和列表元素的状态变化,提供了完整的过渡解决方案。

Vue 在插入、更新或者移除 DOM 时,提供多种不同方式的应用过渡效果。

  • 自定义CSS
  • 第三方 CSS 动画库,如 Animate.css
  • 在过渡钩子函数使用 JavaScript 直接操作 DOM
  • 使用第三方 JavaScript 动画库,如 Velocity.js

单元素/组件的过渡

Vue 提供了 <transition> 组件来包裹单个元素 / 组件,实现进入 / 离开过渡。

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <button @click="show = !show">切换内容</button>

    <transition name="fade">
      <p v-if="show">这是一段会渐变的文本</p>
    </transition>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 创建实例
    var app = new Vue({
      el: "#app",
      data: {
        show: false
      },
      methods: {}
    })
  </script>
  <style>
    .fade-enter-active, .fade-leave-active {
      transition: opacity .5s;
    }

    .fade-enter, .fade-leave-to {
      opacity: 0;
    }
  </style>
</body>

name="fade" 为例:

  • fade-enter:进入过渡开始状态。在元素被插入之前生效,在元素被插入之后的下一帧移除。
  • fade-enter-active:进入过渡生效时的状态(整个进入过程生效)。在过渡/动画完成之后移除。这个类可以被用来定义进入过渡的过程时间,延迟和曲线函数。
  • fade-enter-to:进入过渡结束状态。在元素被插入之后下一帧生效 (与此同时 v-enter 被移除),在过渡/动画完成之后移除。
  • fade-leave:离开过渡开始状态。在离开过渡被触发时立刻生效,下一帧被移除。
  • fade-leave-active:离开过渡活跃状态(整个离开过程生效)在离开过渡被触发时立刻生效,在过渡/动画完成之后移除。这个类可以被用来定义离开过渡的过程时间,延迟和曲线函数。
  • fade-leave-to:离开过渡结束状态态。在离开过渡被触发之后下一帧生效 (与此同时 v-leave 被删除),在过渡/动画完成之后移除。

对于这些在过渡中切换的类名来说,如果你使用一个没有名字的 <transition>,则 v- 是这些类名的默认前缀。如果你使用了 <transition name="fade">,那么 v-enter 会替换为 fade

除了 CSS 过渡,还可以使用 CSS 动画:

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <button @click="show = !show">切换内容</button>

    <transition name="bounce">
      <p v-if="show">这是一段会弹跳的文本</p>
    </transition>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 创建实例
    var app = new Vue({
      el: "#app",
      data: {
        show: false
      },
      methods: {}
    })
  </script>
  <style>
    .bounce-enter-active {
      animation: bounce-in 0.5s;
    }

    .bounce-leave-active {
      animation: bounce-out 0.5s;
    }

    @keyframes bounce-in {
      0% { transform: scale(0); }
      50% { transform: scale(1.5); }
      100% { transform: scale(1); }
    }

    @keyframes bounce-out {
      0% {transform: scale(1); }
      50% { transform: scale(1.5); }
      100% { transform: scale(0); }
    }
  </style>
</body>
  • 自定义过渡的类名

除了使用默认的过渡类名(基于 <transition> 组件的 name 属性),还可以通过自定义过渡类名来更灵活地控制过渡效果,尤其适合结合第三方 CSS 动画库(如 Animate.css)使用。

(1)enter-class:进入过渡的开始状态类

(2)enter-active-class:进入过渡的活跃状态类(整个进入过程)

(3)enter-to-class:进入过渡的结束状态类

(4)leave-class:离开过渡的开始状态类

(5)leave-active-class:离开过渡的活跃状态类(整个离开过程)

(6)leave-to-class:离开过渡的结束状态类

首先引入 Animate.css 库:

css 复制代码
<link href="https://cdn.jsdelivr.net/npm/animate.css@3.7.2/animate.min.css" rel="stylesheet">

在组件中使用自定义类名:

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <button @click="show = !show">切换内容</button>

    <transition enter-active-class="animated tada" leave-active-class="animated bounceOutRight">
      <p v-if="show" class="animated">这是一段带动画的文本</p>
    </transition>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 创建实例
    var app = new Vue({
      el: "#app",
      data: {
        show: false
      },
      methods: {}
    })
  </script>
</body>

自定义类名的优先级高于默认类名,如果同时设置了 name 属性和自定义类名属性,会优先使用自定义类名。可以混合使用:例如保留默认的开始 / 结束类,只自定义活跃类

  • 显性的过渡持续时间

你可以通过duration属性显性地控制过渡持续时间,这比仅依赖 CSS 过渡更精确,尤其是在处理复杂动画时。

(1)数字形式:duration="500" - 进入和离开都使用 500ms

(2)数对象形式:duration="{ enter: 500, leave: 800 }" - 分别设置进入和离开时间

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <button @click="show = !show">切换内容</button>
    <!-- 显性设置持续时间 -->
    <transition :name="transitionType" :duration="transitionDuration">
      <p v-if="show">这是一段会渐变的文本</p>
    </transition>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 创建实例
    var app = new Vue({
      el: "#app",
      data: {
        transitionType: 'fade',
        enterDuration: 2000,
        leaveDuration: 3000,
        show: false
      },
      computed: {
        // 计算属性返回包含进入和离开时间的对象
        transitionDuration() {
          return {
            enter: this.enterDuration,
            leave: this.leaveDuration
          }
        }
      },
    })
  </script>
  <style>
    .fade-enter-active, .fade-leave-active {
       transition: opacity 3s;  /* 使用最长的过渡时间3秒 */
      transition-timing-function: linear; /* 线性过渡更明显 */
    }

    .fade-enter, .fade-leave-to {
      opacity: 0;
    }
  </style>
</body>

实际开发中更推荐:

(1)进入 / 离开动画的 CSS 时间 ≤ Vueduration(避免动画被截断)

(2)最好让两者完全匹配(动画完整且钩子触发时机准确)

  • JavaScript 钩子

可以通过 JavaScript 钩子函数实现更复杂的动画逻辑:

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <button @click="show = !show">切换内容</button>

    <transition
      v-on:before-enter="beforeEnter"
      v-on:enter="enter"
      v-on:after-enter="afterEnter"
      v-on:enter-cancelled="enterCancelled"
      v-on:before-leave="beforeLeave"
      v-on:leave="leave"
      v-on:after-leave="afterLeave"
      v-on:leave-cancelled="leaveCancelled"
    >
      <p v-if="show">JS 动画示例</p>
    </transition>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 创建实例
    var app = new Vue({
      el: "#app",
      data: {
        show: false
      },
      methods: {
          beforeEnter: function (el) {
            // 进入前的准备
            el.style.opacity = 0
          },
          enter: function (el, done) {
            // 进入动画(需要调用 done() 通知结束)
            Velocity(el, { opacity: 1 }, { duration: 500, complete: done })
          },
          // 其他钩子...
      }
    })
  </script>
</body>
  • 初始渲染的过渡

初始渲染的过渡指的是页面加载时元素第一次出现时的过渡效果。可以通过 appear 属性设置节点在初始渲染的过渡。

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <button @click="show = !show">切换内容</button>

    <transition name="fade" appear>
      <p v-if="show">这个元素在初始渲染时会有过渡效果</p>
    </transition>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 创建实例
    var app = new Vue({
      el: "#app",
      data: {
        show: false
      }
    })
  </script>
  <style>
    .fade-enter-active, .fade-leave-active {
      transition: opacity .5s;
    }

    .fade-enter, .fade-leave-to
      {
      opacity: 0;
    }
  </style>
</body>

也可以通过 JavaScript 钩子函数来处理初始渲染的过渡:

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <button @click="show = !show">切换内容</button>

    <transition name="fade" appear v-on:before-appear="beforeAppear" v-on:appear="appear"
      v-on:after-appear="afterAppear">
      <p v-if="show">这个元素在初始渲染时会有过渡效果</p>
    </transition>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 创建实例
    var app = new Vue({
      el: "#app",
      data: {
        show: false
      },
      methods: {
        beforeAppear: function (el) {
          // 初始渲染前的准备
          el.style.transform = 'scale(0)'
        },
        appear: function (el, done) {
          // 初始渲染动画
          setTimeout(() => {
            el.style.transition = 'transform 0.5s'
            el.style.transform = 'scale(1)'
            done() // 通知动画结束
          }, 100)
        },
        afterAppear: function (el) {
          // 初始渲染动画完成后
          console.log('初始渲染过渡完成')
        }
      }
    })
  </script>
  <style>
    .fade-enter-active,
    .fade-leave-active {
      transition: opacity .5s;
    }

    .fade-enter,
    .fade-leave-to {
      opacity: 0;
    }
  </style>
</body>
  • 多个元素的过渡

处理多个元素的过渡有几种常见场景和实现方式,主要取决于元素是通过条件渲染(v-if/v-else)还是动态组件切换。

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <button @click="show = !show">切换内容</button>

    <transition name="fade">
      <button v-if="show" key="save" @click="show = false">
        保存
      </button>
      <button v-else key="edit" @click="show = true">
        编辑
      </button>
    </transition>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 创建实例
    var app = new Vue({
      el: "#app",
      data: {
        show: false
      },
      methods: {
      }
    })
  </script>
  <style>
    .fade-enter-active, .fade-leave-active {
      transition: opacity .5s;
    }

    .fade-enter, .fade-leave-to {
      opacity: 0;
    }
  </style>
</body>

当有相同标签名的元素切换时,需要通过 key 属性设置唯一的值来标记以让 Vue 区分它们,否则 Vue 为了效率只会替换相同标签内部的内容。即使在技术上没有必要,给在 <transition> 组件中的多个元素设置 key 是一个更好的实践。

  • 过度模式

过渡模式(mode) 用于控制多个元素切换时的过渡行为,解决元素在进入 / 离开过程中可能出现的重叠或布局抖动问题(就像上面的代码一样,一个离开过渡的时候另一个开始进入过渡。这是 <transition> 的默认行为 - 进入和离开同时发生)。

过渡模式仅适用于 <transition> 组件(不适用于 <transition-group>),主要用于处理互斥元素的切换场景(如 v-if/v-else 切换、动态组件切换等)。

(1)out-in:先完成当前元素的离开过渡,再开始新元素的进入过渡

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <button @click="show = !show">切换内容</button>

    <transition name="fade" mode="out-in">
      <button v-if="show" key="save" @click="show = false">
        保存
      </button>
      <button v-else key="edit" @click="show = true">
        编辑
      </button>
    </transition>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 创建实例
    var app = new Vue({
      el: "#app",
      data: {
        show: false
      },
      methods: {
      }
    })
  </script>
  <style>
    .fade-enter-active, .fade-leave-active {
      transition: opacity .5s;
    }

    .fade-enter, .fade-leave-to {
      opacity: 0;
    }
  </style>
</body>

(2)in-out:先开始新元素的进入过渡,再完成当前元素的离开过渡(较少用)

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <button @click="show = !show">切换内容</button>

    <transition name="fade" mode="in-out">
      <button v-if="show" key="save" @click="show = false">
        保存
      </button>
      <button v-else key="edit" @click="show = true">
        编辑
      </button>
    </transition>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 创建实例
    var app = new Vue({
      el: "#app",
      data: {
        show: false
      },
      methods: {
      }
    })
  </script>
  <style>
    .fade-enter-active, .fade-leave-active {
      transition: opacity .5s;
    }

    .fade-enter, .fade-leave-to {
      opacity: 0;
    }
  </style>
</body>

只用添加一个简单的属性,就解决了之前的过渡问题而无需任何额外的代码。

  • 多个组件的过渡

多个组件之间的过渡切换可以通过 <transition> 组件配合动态组件(:is 语法)实现,让组件在切换时呈现平滑的过渡效果。

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <button @click="change">切换内容</button>

    <transition name="fade" mode="out-in">
      <component v-bind:is="view"></component>
    </transition>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 创建实例
    var app = new Vue({
      el: "#app",
      data: {
        show: false,
        view: 'v-a'
      },
      methods: {
        change() {
          this.show = !this.show
          this.view = this.show ? 'v-a' : 'v-b'
        }
      },
      components: {
        'v-a': {
          template: '<div>Component A</div>'
        },
        'v-b': {
          template: '<div>Component B</div>'
        }
      }
    })
  </script>
  <style>
    .fade-enter-active, .fade-leave-active {
      transition: opacity .5s;
    }

    .fade-enter, .fade-leave-to {
      opacity: 0;
    }
  </style>
</body>

列表过渡

目前为止,关于过渡我们已经讲到单个节点和同一时间渲染多个节点中的一个。那么怎么同时渲染整个列表,比如使用 v-for?在这种场景中,使用 <transition-group> 组件。

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <button @click="add">添加内容</button>
    <button @click="del">删除内容</button>
    <transition-group name="list" tag="ul">
      <li v-for="item in items" :key="item">
        {{ item }}
      </li>
    </transition-group>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 创建实例
    var app = new Vue({
      el: "#app",
      data: {
        nextNum: 11,
        items: [1,2,3,4,5,6,7,8,9,10]
      },
      methods: {
        randomIndex: function () {
          return Math.floor(Math.random() * this.items.length)
        },
        add() {
          this.items.splice(this.randomIndex(), 0, this.nextNum++)
        },
        del() {
          this.items.splice(this.randomIndex(), 1)
        }
      },
      components: {
        'v-a': {
          template: '<div>Component A</div>'
        },
        'v-b': {
          template: '<div>Component B</div>'
        }
      }
    })
  </script>
  <style>
    .list-enter-active, .list-leave-active {
      transition: all 0.5s;
    }

    .list-enter, .list-leave-to {
      opacity: 0;
      transform: translateY(30px);
    }
  </style>
</body>

这个例子有个问题,当添加和移除元素的时候,周围的元素会瞬间移动到他们的新布局的位置,而不是平滑的过渡,我们下面会解决这个问题。

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <button @click="add">添加内容</button>
    <button @click="del">删除内容</button>
    <transition-group name="list" tag="ul">
      <li v-for="item in items" :key="item">
        {{ item }}
      </li>
    </transition-group>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 创建实例
    var app = new Vue({
      el: "#app",
      data: {
        nextNum: 11,
        items: [1,2,3,4,5,6,7,8,9,10]
      },
      methods: {
        randomIndex: function () {
          return Math.floor(Math.random() * this.items.length)
        },
        add() {
          this.items.splice(this.randomIndex(), 0, this.nextNum++)
        },
        del() {
          this.items.splice(this.randomIndex(), 1)
        }
      },
      components: {
        'v-a': {
          template: '<div>Component A</div>'
        },
        'v-b': {
          template: '<div>Component B</div>'
        }
      }
    })
  </script>
  <style>
    .list-move {
      transition: transform 0.5s ease;
    }
    .list-enter-active, .list-leave-active {
      transition: all 0.5s;
    }

    .list-enter, .list-leave-to {
      opacity: 0;
      transform: translateY(30px);
    }
  </style>
</body>

<transition-group> 组件还有一个特殊之处。不仅可以进入和离开动画,还可以改变定位。要使用这个新功能只需了解新增的 v-move class ,它会在元素的改变定位的过程中应用。像之前的类名一样,可以通过 name 属性来自定义前缀,也可以通过 move-class 属性 手动设置。

通过为该类添加 transition: transform ... 实现移动动画。

可复用的过渡

过渡可以通过 Vue 的组件系统实现复用。要创建一个可复用过渡组件,你需要做的就是将 <transition> 或者 <transition-group> 作为根组件,然后将任何子组件放置在其中就可以了。

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <h2>可复用过渡组件示例</h2>

    <!-- 使用淡入淡出过渡 -->
    <reusable-transition name="fade">
      <div class="box" v-if="showFade">淡入淡出过渡内容</div>
    </reusable-transition>
    <button @click="showFade = !showFade">切换淡入淡出</button>

    <!-- 使用滑动过渡 -->
    <reusable-transition name="slide">
      <div class="box" v-if="showSlide">滑动过渡内容</div>
    </reusable-transition>
    <button @click="showSlide = !showSlide">切换滑动效果</button>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 定义可复用的过渡组件
    Vue.component('reusable-transition', {
      props: ['name'],
      template: 
        `
        <transition :name="name">
            <slot></slot> <!-- 插槽用于放置需要过渡的内容 -->
        </transition>
        `
    });

    // 创建Vue实例
    new Vue({
      el: '#app',
      data() {
        return {
          showFade: false,
          showSlide: false
        }
      }
    });
  </script>
  <style>
    /* 淡入淡出过渡样式 */
    .fade-enter-active, .fade-leave-active {
      transition: opacity 0.5s;
    }

    .fade-enter, .fade-leave-to {
      opacity: 0;
    }

    /* 滑动过渡样式 */
    .slide-enter-active, .slide-leave-active {
      transition: all 0.5s;
    }

    .slide-enter, .slide-leave-to {
      transform: translateX(30px);
      opacity: 0;
    }
  </style>
</body>

动态过渡

Vue 中即使是过渡也是数据驱动的!动态过渡最基本的例子是通过 name 属性来绑定动态值。这可以通过动态绑定过渡名称、动态样式或使用 JavaScript 钩子来实现。

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <h2>动态过渡示例</h2>

    <div class="controls">
      <!-- 选择过渡类型 -->
      <select v-model="transitionType">
        <option value="fade">淡入淡出</option>
        <option value="slide">滑动</option>
        <option value="scale">缩放</option>
        <option value="rotate">旋转</option>
      </select>

      <!-- 控制显示/隐藏 -->
      <button @click="show = !show">
        {{ show ? '隐藏内容' : '显示内容' }}
      </button>
    </div>

    <!-- 动态过渡组件 -->
    <transition :name="transitionType" :duration="dynamicDuration">
      <div class="content" v-if="show">
        这是一段可以应用动态过渡效果的内容。
        选择不同的过渡类型,会看到不同的动画效果。
      </div>
    </transition>

    <!-- 动态调整过渡时长 -->
    <div>
      <label>过渡时长: {{ transitionDuration }}ms</label>
      <input type="range" min="100" max="2000" v-model="transitionDuration" @input="updateDuration">
    </div>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    new Vue({
      el: '#app',
      data() {
        return {
          show: false,
          transitionType: 'fade', // 默认过渡类型
          transitionDuration: 500,
          dynamicDuration: 500
        }
      },
      methods: {
        updateDuration() {
          // 可以为进入和离开设置不同的时长
          this.dynamicDuration = {
            enter: this.transitionDuration,
            leave: this.transitionDuration * 0.8 // 离开动画稍快
          }
        }
      }
    });
  </script>
  <style>
    /* 淡入淡出过渡 */
    .fade-enter-active, .fade-leave-active {
      transition: opacity 0.5s ease;
    }

    .fade-enter, .fade-leave-to {
      opacity: 0;
    }

    /* 滑动过渡 */
    .slide-enter-active, .slide-leave-active {
      transition: all 0.5s ease;
    }

    .slide-enter, .slide-leave-to {
      transform: translateX(50px);
      opacity: 0;
    }

    /* 缩放过渡 */
    .scale-enter-active, .scale-leave-active {
      transition: all 0.3s ease;
    }

    .scale-enter, .scale-leave-to {
      transform: scale(0.8);
      opacity: 0;
    }

    /* 旋转过渡 */
    .rotate-enter-active, .rotate-leave-active {
      transition: all 0.5s ease;
    }

    .rotate-enter, .rotate-leave-to {
      transform: rotate(180deg) scale(0.5);
      opacity: 0;
    }
  </style>
</body>

状态过渡

状态过渡系统提供了灵活的方式来为元素的进入、离开和状态变化添加动画效果。比如:数字和运算、颜色的显示等。

这些数据要么本身就以数值形式存储,要么可以转换为数值。有了这些数值后,我们就可以结合 Vue 的响应式和组件系统,使用第三方库来实现切换元素的过渡状态。

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <input v-model.number="number" type="number" step="20">
    <p>{{ animatedNumber }}</p>
  </div>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.2.4/gsap.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 创建实例
    var app = new Vue({
      el: "#app",
      data: {
        number: 0,
        tweenedNumber: 0
      },
      computed: {
        animatedNumber: function () {
          return this.tweenedNumber.toFixed(0);
        }
      },
      watch: {
        number: function (newValue) {
          gsap.to(this.$data, { duration: 0.5, tweenedNumber: newValue });
        }
      }
    })
  </script>
</body>
  • 动态状态过渡

就像 Vue 的过渡组件一样,数据背后状态过渡会实时更新,这对于原型设计十分有用。利用 CSS 过渡实现背景颜色随状态变化的平滑过渡。

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <!-- 状态变化过渡 -->
    <div class="container">
      <button @click="changeColor">改变颜色</button>

      <div class="demo-box">
        <div class="color-box" :style="{ backgroundColor: currentColor }"></div>
        <p>当前颜色: {{ currentColor }}</p>
      </div>
    </div>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 创建实例
    var app = new Vue({
      el: "#app",
      data: {
        // 颜色状态
        colors: ['#3498db', '#e74c3c', '#2ecc71', '#f39c12', '#9b59b6'],
        currentColor: '#3498db'
      },
      methods: {
        // 颜色变化
        changeColor() {
          const currentIndex = this.colors.indexOf(this.currentColor);
          const nextIndex = (currentIndex + 1) % this.colors.length;
          this.currentColor = this.colors[nextIndex];
        }
      }
    })
  </script>
  <style>
    /* 颜色过渡 */
    .color-box {
      width: 100px;
      height: 100px;
      border-radius: 6px;
      transition: background-color 1s ease;
      margin: 10px 0;
    }
  </style>
</body>

管理太多的状态过渡会很快的增加 Vue 实例或者组件的复杂性,幸好很多的动画可以提取到专用的子组件。

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <div class="container">
      <!-- 使用子组件 -->
      <color-transition :colors=colors :current-color=initialColor></color-transition>
    </div>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 定义颜色过渡子组件
    Vue.component('color-transition', {
      props: {
        'colors': Array,
        'currentColor': String
      },
      template: `
                <div>
                    <button @click="changeColor">改变颜色</button>
                    
                    <div class="demo-box">
                        <div class="color-box" :style="{ backgroundColor: localCurrentColor }"></div>
                        <p>当前颜色: {{ localCurrentColor }}</p>
                    </div>
                </div>
            `,
      data() {
        return {
          // 创建本地数据属性,基于prop的值初始化
          localCurrentColor: this.currentColor
        }
      },
      methods: {
        // 颜色变化方法
        changeColor() {
          const currentIndex = this.colors.indexOf(this.localCurrentColor);
          const nextIndex = (currentIndex + 1) % this.colors.length;
          this.localCurrentColor = this.colors[nextIndex];
        }
      }
    });

    // 创建Vue实例(父组件)
    var app = new Vue({
      el: "#app",
      data: {
        // 父组件提供的颜色列表
        colors: ['#3498db', '#e74c3c', '#2ecc71', '#f39c12', '#9b59b6'],
        // 初始颜色
        initialColor: '#3498db',
      }
    });
  </script>
  <style>
    /* 颜色过渡 */
    .color-box {
      width: 100px;
      height: 100px;
      border-radius: 6px;
      transition: background-color 1s ease;
      margin: 10px 0;
    }
  </style>
</body>

只要一个动画,就可以带来生命。Vue 可以帮到你。因为 SVG 的本质是数据,我们只需要这些动物兴奋、思考或警戒的样例。然后 Vue 就可以辅助完成这几种状态之间的过渡动画,来制作你的欢迎页面、加载指示、以及更加带有情感的提示。

可复用性 & 组合

"可复用性" 指将重复的逻辑、组件或功能抽离,供多个地方调用以减少冗余;"组合" 则指将抽离的可复用单元(逻辑 / 组件)灵活整合,构建复杂功能。

混入

混入 (mixin) 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。允许将组件中的可复用逻辑(如数据、方法、生命周期钩子等)提取到独立的模块中,然后在多个组件中 "混入" 使用。这种方式可以有效减少代码冗余,提高逻辑复用性。

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <component-a></component-a>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 定义一个混入对象
    var logMixin = {
      created: function () {
        this.hello()
      },
      methods: {
        hello: function () {
          console.log('hello from mixin!')
        }
      }
    }
    // 组件 A 使用日志混入
    Vue.component('component-a', {
      name: 'ComponentA',
      mixins: [logMixin], // 引入混入
      created() {
        console.log('开始初始化数据'); // 调用混入中的方法
      },
      data() {
        return { count: 0 };
      },
      template: '<div>这是一个模板内容</div>'
    });
    // 创建Vue实例(父组件)
    var app = new Vue({
      el: "#app",
      data: {}
    });
  </script>
</body>

执行结果如图:

  • 合并规则

当混入与组件存在同名选项时,Vue 会按规则合并,避免冲突。

数据对象(dataprops)在内部会进行递归合并,并在发生冲突时以组件数据优先。

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <component-a></component-a>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 定义一个混入对象
    var logMixin = {
      data: function () {
        return {
          message: 'hello',
          foo: 'abc'
        }
      }
    }
    // 组件 A 使用日志混入
    Vue.component('component-a', {
      name: 'ComponentA',
      mixins: [logMixin], // 引入混入,
      created() {
        console.log(this.$data)
        // => { message: "goodbye", foo: "abc", bar: "def" }
      },
      data() {
        return {
          message: 'goodbye',
          bar: 'def'
        }
      },
      template: '<div>这是一个模板内容</div>'
      
    });
    // 创建Vue实例(父组件)
    var app = new Vue({
      el: "#app",
      data: {}
    });
  </script>
</body>

methodscomputed 同名方法 / 计算属性,组件的定义覆盖混入的定义。

html 复制代码
<body>
    <!-- 父组件 -->
    <div id="app">
        <component-a></component-a>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
    <script>
        // 定义一个混入对象
        var logMixin = {
            methods: {
                mixinMethod() {
                    console.log('混入的method')
                }
            },
            computed: {
                mixinComputed() {
                    return '混入计算属性的值';
                }
            }
        }
        // 组件 A 使用日志混入
        Vue.component('component-a', {
            name: 'ComponentA',
            mixins: [logMixin], // 引入混入,
            created() {
                this.mixinMethod();
                console.log(this.mixinComputed);  // 会执行组件的computed
            },
            methods: {
                mixinMethod() {
                    console.log('组件的method')
                }
            },
            computed: {
                mixinComputed() {
                    return '组件计算属性的值';
                }
            },
            template: '<div>这是一个模板内容</div>'

        });
        // 创建Vue实例(父组件)
        var app = new Vue({
            el: "#app",
            data: {}
        });
    </script>
</body>

生命周期钩子 混入和组件的同名钩子(如 createdwatch)会全部执行,混入钩子先于组件钩子执行。

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <component-a></component-a>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 定义一个混入对象
    var logMixin = {
      created() {
        console.log('混入的created')
      }
    }
    // 组件 A 使用日志混入
    Vue.component('component-a', {
      name: 'ComponentA',
      mixins: [logMixin], // 引入混入,
      created() {
        console.log('组件的created')
      },
      template: '<div>这是一个模板内容</div>'
      
    });
    // 创建Vue实例(父组件)
    var app = new Vue({
      el: "#app",
      data: {}
    });
  </script>
</body>
  • 全局混入

全局混入(Global Mixin)是一种特殊的混入方式,它会影响所有 Vue 组件(包括第三方组件和内置组件),可以在全局范围内注入共享逻辑。

html 复制代码
<body>
    <!-- 父组件 -->
    <div id="app">
        <component-a></component-a>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
    <script>
        Vue.mixin({
            // 所有组件都会拥有这些选项
            data() {
                return {
                    globalData: '这是全局混入的数据'
                }
            },
            created() {
                console.log('全局混入的 created 钩子执行')
            },
            methods: {
                mixinMethod() {
                    console.log('混入的method')
                }
            }
        })
        // 组件 A 使用日志混入
        Vue.component('component-a', {
            name: 'ComponentA',
            data() {
                return {
                    message: '内部消息'
                }
            },
            created() {
                console.log(this.$data); // => {globalData: '这是全局混入的数据',message:'内部消息'}
            },
            methods: {
                mixinMethod() {
                    console.log('组件的method')
                }
            },
            computed: {
                mixinComputed() {
                    return '组件计算属性的值';
                }
            },
            template: '<div>这是一个模板内容</div>'

        });
        // 创建Vue实例(父组件)
        var app = new Vue({
            el: "#app",
            data: {}
        });
    </script>
</body>

执行结果如图:

大多数情况下,只应当应用于自定义选项,就像上面示例一样。推荐将其作为插件发布,以避免重复应用混入。

  • 自定义选项合并策略

自定义选项将使用默认策略,即简单地覆盖已有值。如果想让自定义选项 以自定义逻辑合并,可以向 Vue.config.optionMergeStrategies 添加一个函数:

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <component-a></component-a>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 定义myOption(自定义选项)的合并策略:对象属性合并
    Vue.config.optionMergeStrategies.myOption = function (toVal, fromVal) {
      // 如果子组件没有该选项,直接使用父组件的
      if (!toVal) return fromVal
      // 如果父组件没有该选项,直接使用子组件的
      if (!fromVal) return toVal
      // 否则合并两个对象(浅合并,如需深合并可使用工具函数)
      return Object.assign({}, fromVal, toVal)
    }

    // 定义一个混入对象
    var logMixin = {
      myOption: {
        message: 'hello',
        foo: 'abc'
      }
    }
    // 组件 A 使用日志混入
    Vue.component('component-a', {
      name: 'ComponentA',
      mixins: [logMixin], // 引入混入
      myOption: {
        message: 'goodbye',
        bar: 'def'
      },
      created() {
        console.log(this.$options.myOption)
        // >= {message: 'hello', bar: 'def', foo: 'abc'}
      },
      template: '<div>这是一个模板内容</div>'

    });
    // 创建Vue实例(父组件)
    var app = new Vue({
      el: "#app",
      data: {}
    });
  </script>
</body>

继承

extends 是组件的核心选项之一,用于扩展(继承)另一个组件的配置,实现组件逻辑的复用与扩展,是比 mixins 更轻量、更聚焦的 "单继承" 方案。

html 复制代码
<body>
  <div id="app">
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 定义extends
    const localBase = {
      data() {
        return {
          baseData: '父类数据',
          overData: '覆盖数据'
        }
      },
      created() {
        console.info('父类的 created 钩子执行')
      },
      methods: {
        baseMethod() {
          console.log('父类的方法')
        }
      }
    }
    // 创建Vue实例
    new Vue({
      el: "#app",
      data: {
        overData: '子类覆盖数据'
      },
      created() {
        console.log(this.$data); // => {baseData: '父类数据',overData: '子类覆盖数据'}
        this.baseMethod(); // 覆盖子类的方法
      },
      methods: {
        baseMethod() {
          console.log('子类覆盖方法');
        }
      },
      extends: localBase,
      template: `<div>{{baseData}}-{{overData}}</div>`
    })
  </script>
</body>

Vue 内部会将 extends 目标组件的选项与当前组件选项合并,合并规则与 mixins 一致。但 extends 优先级高于 mixins、低于当前组件。

html 复制代码
<body>
  <div id="app">
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 定义extends
    const localBase = {
      data() {
        return {
          testData: '父类数据'
        }
      },
      created() {
        console.info('父类的 created 钩子执行')
      },
      methods: {
        test() {
          console.log('父类的方法')
        }
      }
    }
    // 定义minix
    const localMinix = {
      data() {
        return {
          testData: 'minix数据'
        }
      },
      created() {
        console.info('minix 的 created 钩子执行')
      },
      methods: {
        test() {
          console.log('minix 的方法')
        }
      }

    }
    // 创建Vue实例
    new Vue({
      el: "#app",
      data: {
        testData: '子类覆盖数据'
      },
      created() {
        console.log(this.$data); // => {testData: '子类覆盖数据'}
        this.test(); // minix 的方法
      },
      extends: localBase,
      mixins:[localMinix]
    })
  </script>
</body>

先合并的先执行,会先执行extendscreated钩子函数,后合并的覆盖先合并的,mixins后合会覆盖extends,最后当前组件最后合,钩子最后执行。

  • 全局继承

Vue.extend() 是全局 API ,用于基于 Vue 构造器创建一个组件构造器(子类),是 Vue 底层创建组件的核心方式之一。

html 复制代码
<body>
  <div id="app">
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    const UserCard = Vue.extend({
      // 组件选项(和普通 Vue 组件一致)
      props: {
        // 接收外部传入的属性
        username: {
          type: String,
          required: true
        },
        age: {
          type: Number,
          default: 18
        }
      },
      data() {
        return {
          isShow: true
        }
      },
      template: `
        <div class="user-card" v-if="isShow">
          <h3>用户名:{{ username }}</h3>
          <p>年龄:{{ age }}</p>
          <button @click="hideCard">隐藏卡片</button>
        </div>
      `,
      methods: {
        hideCard() {
          this.isShow = false;
        }
      }
    })
    // 方式1:实例化组件并挂载到 DOM
    const instance = new UserCard({
      propsData: { // 传递 props(仅实例化时可用)
        username: '张三'
      }
    }).$mount('#app');
    
    // 方式2:作为组件注册使用(全局/局部)
    // Vue.component('my-component', UserCard); // 全局注册
    // 或局部注册
    // new Vue({
    //   el: '#app',
    //   data: {
    //     username: '张三',
    //   },
    //   components: {
    //     'my-component': UserCard
    //   }
    // })

  </script>
</body>

最常用的场景 ------ 无需在模板中写组件标签,通过代码动态创建并挂载组件。

extendsmixins 都是 Vue 2 中复用组件逻辑的方式,但定位和用法有明显区别:

  1. extends单继承(仅能扩展一个组件);mixins多混入(可传入多个 mixin 数组)。
  2. extends强调 "继承 / 扩展",适合主从关系(如基础组件 → 业务组件);mixins强调 "混入",适合通用逻辑复用(如防抖、埋点)
  3. extends优先级高于 mixins,低于当前组件;mixins最低(低于 extends 和当前组件)

自定义指令

渐进式JavaScript框架:Vue中介绍过框架自带指令。自定义指令是一种用于操作 DOM 的强大工具,允许你为元素添加自定义行为。它可以扩展 VueDOM 元素的控制能力,常用于处理焦点、表单验证、滚动监听等场景。

html 复制代码
<body>

  <div id="app" v-cloak>
    <h1>原始值:{{num}}</h1>
    <h1 v-big="num">变大值:{{num}}</h1>
    <button @click="num++">修改值</button>
  </div>

  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    new Vue({
      el: "#app",
      data: {
        num: 1
      },
      directives:{
        big:function(el,binding) {
          el.innerHTML = binding.value * 10
        }
      }
    })
  </script>
</body>

自定义指令会在与元素绑定时进行第一次调用,其次就是再模板被重新解析时调用(无论修改自己的值或者是别的值都会触发调用)

自定义指令可以注册为全局指令局部指令,其核心是定义一个包含钩子函数的对象。

  • 全局指令
html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <input v-focus type="text" placeholder="自动聚焦的输入框">
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 注册全局指令 v-focus
    Vue.directive('focus', {
      // 指令钩子函数
      inserted: function (el) {
        // 当元素插入到 DOM 时自动聚焦
        el.focus()
      }
    })
    // 创建Vue实例(父组件)
    var app = new Vue({
      el: "#app",
      data: {}
    });
  </script>
</body>

页面加载后,带有 v-focus 的输入框会自动获得焦点,光标会出现在输入框内,用户可直接输入内容。

  • 局部指令

如果想注册局部指令,组件中也接受一个 directives 的选项:

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <input v-focus type="text" placeholder="自动聚焦的输入框">
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 创建Vue实例(父组件)
    var app = new Vue({
      el: "#app",
      data: {},
      // 局部指令 v-focus
      directives: {
        focus: {
          inserted: function (el) {
            el.focus()
          }
        }
      }
    });
  </script>
</body>
  • 钩子函数

一个指令定义对象(全局指令和局部指令都有)可以提供如下几个钩子函数 (均为可选):

(1)bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。

(2)inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。

(3)update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新。

(4)componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。

(5)unbind:只调用一次,指令与元素解绑时调用。

js 复制代码
// 全局指令
Vue.directive('directiveName', {
  // 指令绑定到元素时调用(只调用一次)
  bind(el, binding, vnode) {},
  
  // 元素插入到 DOM 时调用
  inserted(el, binding, vnode) {},
  
  // 指令模板被重新解析时调用
  update(el, binding, vnode, oldVnode) {},
  
  // 指令模板全部更新后调用
  componentUpdated(el, binding, vnode, oldVnode) {},
  
  // 指令与元素解绑时调用(只调用一次)
  unbind(el, binding, vnode) {}
});
  • 钩子函数参数

指令钩子函数会被传入以下参数:

(1)el:指令所绑定的元素,可以用来直接操作 DOM

(2)binding:一个对象,包含以下属性:

  • name:指令名,不包括 v- 前缀。
  • value:指令的绑定值,例如:v-color="red" 中,值为 red
  • oldValue:指令绑定的前一个值,仅在 updatecomponentUpdated 钩子中可用。无论值是否改变都可用。
  • expression:字符串形式的指令表达式。例如 v-color="color"中,表达式为 "color"
  • arg:传给指令的参数,可选。例如 v-color:bg="red" 中,参数为 "bg"
  • modifiers:一个包含修饰符的对象。例如:v-color.stop="red" 中,modifiers{ stop: true }

(3)vnodeVue 编译生成的虚拟节点。

(4)oldVnode:上一个虚拟节点,仅在 updatecomponentUpdated 钩子中可用。

以开头的代码为例,输出一下elbinding,如图所示:

除了 el 之外,其它参数都应该是只读的,切勿进行修改。

javascript 复制代码
    // 注册全局指令 v-focus
    Vue.directive('focus', {
      // 指令绑定到元素时调用
      bind(el, binding, vnode) {
        console.log('===== bind 钩子 =====');

        // el:绑定的DOM元素(可直接操作)
        el.style.color = 'blue'; // 改变元素颜色

        // binding:指令信息对象
        console.log('binding.value:', binding.value); // 输出:"原始消息"(指令绑定值)
        console.log('binding.expression:', binding.expression); // 输出:"message"(表达式字符串)
        console.log('binding.arg:', binding.arg); // 输出:"foo"(指令参数)
        console.log('binding.modifiers:', binding.modifiers); // 输出:{ bar: true }(修饰符)

        // vnode:虚拟节点(包含组件相关信息)
        console.log('vnode.key:', vnode.key); // 输出:undefined(无key时)
        console.log('vnode.context:', vnode.context); // 输出:当前Vue实例
        console.log('vnode.data.attrs:', vnode.data.attrs); // 输出:{ "data-id": "123" }
      },
      // 元素插入DOM时调用
      inserted(el) {
        console.log('===== inserted 钩子 =====');
        console.log('元素是否在DOM中:', document.contains(el)); // 输出:true
      },
      // 指令模板被重新解析时调用
      update(el, binding, vnode, oldVnode) {
        console.log('===== update 钩子 =====');
        console.log('旧值:', oldVnode.data.directives[0].value); // 输出更新前的值
        console.log('新值:', binding.value); // 输出更新后的值
      },
      // 指令模板全部更新后调用
      componentUpdated(el) {
        console.log('===== componentUpdated 钩子 =====');
        console.log('元素最终文本:', el.textContent); // 输出更新后的文本
      },
      // 指令与元素解绑时调用
      unbind(el) {
        console.log('===== unbind 钩子 =====');
        console.log('元素已解绑指令');
      }
    })
  • 动态指令参数

动态指令参数允许你将指令的参数(arg)设置为响应式的,通过数据动态改变参数值,使指令更加灵活。

例如你想要创建一个自定义指令,用来通过固定布局将元素固定在页面上。我们可以像这样创建一个通过指令值来更新竖直位置像素值的自定义指令:

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <p>Scroll down the page</p>
    <p v-position="200">Stick me 200px from the top of the page</p>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 创建Vue实例(父组件)
    var app = new Vue({
      el: "#app",
      data: {},
      directives: {
        position: {
          bind: function (el, binding, vnode) {
            el.style.position = 'fixed'
            el.style.top = binding.value + 'px'
          }
        }
      }
    });
  </script>
</body>

但如果场景是我们需要把元素固定在左侧而不是顶部又该怎么办呢?这时使用动态参数就可以非常方便地根据每个组件实例来进行更新。

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <button @click="toggleDirection">
      切换方向(当前: {{ direction }})
    </button>
    <p v-position:[direction]="200">I am pinned onto the page at 200px to the left.</p>

  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 创建Vue实例(父组件)
    var app = new Vue({
      el: "#app",
      data: {
        direction: 'left'
      },
      methods: {
        // 点击按钮时切换方向
        toggleDirection() {
          this.direction = this.direction === 'left' ? 'right' : 'left';
          console.log('切换方向为:', this.direction); // 验证是否触发
        }
      },
      directives: {
        position: {
          bind: function (el, binding, vnode) {
            el.style.position = 'fixed'
            var s = (binding.arg == 'left' ? 'left' : 'right')
            el.style[s] = binding.value + 'px'
          },
          // 当参数或绑定值变化时调用
          update(el, binding, vnode, oldVnode) {
            // 检查参数是否变化(关键!)
            const oldArg = oldVnode.data.directives[0].arg;
            // 先清除旧的定位样式,再应用新样式
            el.style[oldArg] = '';
            if (binding.arg !== oldArg) {
              el.style.position = 'fixed'
              var s = (binding.arg == 'left' ? 'left' : 'right')
              el.style[s] = binding.value + 'px'
            }
          }
        }
      }
    });
  </script>
</body>
  • 函数简写

当自定义指令的 bindupdate 钩子函数逻辑相同时,可以使用函数简写形式,无需分别定义两个钩子。这种简写会让同一个函数在 bindupdate 时都执行。

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <button @click="toggleDirection">
      切换方向(当前: {{ direction }})
    </button>
    <p v-position:[direction]="200">I am pinned onto the page at 200px to the left.</p>

  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 创建Vue实例(父组件)
    var app = new Vue({
      el: "#app",
      data: {
        direction: 'left'
      },
      methods: {
        // 点击按钮时切换方向
        toggleDirection() {
          this.direction = this.direction === 'left' ? 'right' : 'left';
          console.log('切换方向为:', this.direction); // 验证是否触发
        }
      },
      directives: {
      	// 函数简写形式:同时处理 bind 和 update 逻辑
        position(el, binding, vnode, oldVnode) {
          el.style.position = 'fixed';

          // 关键修复:只在存在oldVnode时才处理旧样式(更新阶段)
          if (oldVnode) { // 绑定阶段oldVnode为undefined,跳过此逻辑
            const oldDirectives = oldVnode.data.directives;
            // 额外判断directives是否存在,避免再次报错
            if (oldDirectives && oldDirectives.length > 0) {
              const oldArg = oldDirectives[0].arg;
              if (binding.arg !== oldArg) {
                el.style[oldArg] = ''; // 清除旧样式
              }
            }
          }

          // 应用新样式(绑定和更新阶段都执行)
          const s = binding.arg === 'left' ? 'left' : 'right';
          el.style[s] = binding.value + 'px';
        }
      }
    });
  </script>
</body>
  • 对象字面量对象字面量

定义指令可以接受对象字面量作为参数,这允许你传递多个值给指令。对象字面量形式让指令的配置更加灵活,适用于需要多个参数的场景。

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <p>Scroll down the page</p>
    <p v-position="{ color: 'red', gap: '200' }">Stick me 200px from the top of the page</p>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 创建Vue实例(父组件)
    var app = new Vue({
      el: "#app",
      data: {},
      directives: {
        position: {
          bind: function (el, binding, vnode) {
            el.style.position = 'fixed'
            el.style.color = binding.value.color
            el.style.top = binding.value.gap + 'px'
          }
        }
      }
    });
  </script>
</body>

当然,你也可以再data属性中定义数据。

渲染函数 & JSX

Vue 推荐在绝大多数情况下使用模板来创建你的 HTML 。然而在一些场景中,你真的需要 JavaScript 的完全编程的能力。这时你可以用渲染函数,它比模板更接近编译器。

让我们深入一个简单的例子:

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <anchored-heading :level="2">Hello world!</anchored-heading>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    Vue.component('anchored-heading', {
      template: `
      <h1 v-if="level === 1">
        <slot></slot>
      </h1>
      <h2 v-else-if="level === 2">
        <slot></slot>
      </h2>
      <h3 v-else-if="level === 3">
        <slot></slot>
      </h3>
      <h4 v-else-if="level === 4">
        <slot></slot>
      </h4>
      <h5 v-else-if="level === 5">
        <slot></slot>
      </h5>
      <h6 v-else-if="level === 6">
        <slot></slot>
      </h6>
      `,
      props: {
        level: {
          type: Number,
          required: true
        }
      }
    })
    // 创建Vue实例(父组件)
    var app = new Vue({
      el: "#app",
      data: {}
    });
  </script>
</body>

动态生成标题 (heading) 的组件时,你可能很快想到通过props定义 level 属性。这里用模板并不是最好的选择:不但代码冗长,而且在每一个级别的标题中重复书写了 <slot></slot>

插槽

虽然模板在大多数组件中都非常好用,但是显然在这里它就不合适了。那么,我们来尝试使用 render 函数重写上面的例子:

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <anchored-heading :level="2">Hello world!</anchored-heading>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    Vue.component('anchored-heading', {
      render: function (createElement) {
        return createElement('h' + this.level, this.$slots.default)
      },
      props: {
        level: {
          type: Number,
          required: true
        }
      }
    })
    // 创建Vue实例(父组件)
    var app = new Vue({
      el: "#app",
      data: {}
    });
  </script>
</body>

看起来简单多了!这样代码精简很多,但是需要非常熟悉 Vue 的实例属性。

也可以通过 this.$scopedSlots 访问作用域插槽,每个作用域插槽都是一个返回若干 VNode 的函数:

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <anchored-heading :level="2" :message="'Hello world!'">
      <template slot-scope="slotProps">
         Level: {{ slotProps.level }}, 内容: {{ slotProps.message }}
      </template>
    </anchored-heading>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    Vue.component('anchored-heading', {
      render: function (createElement) {
        return createElement('h' + this.level, this.$scopedSlots.default({
          level: this.level,
          message: this.message
        }))
      },
      props: ['level', 'message']
    })
    // 创建Vue实例(父组件)
    var app = new Vue({
      el: "#app",
      data: {}
    });
  </script>
</body>

如果要用渲染函数向子组件中传递作用域插槽,可以利用 VNode 数据对象中的 scopedSlots 字段:

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <anchored-heading :level="2" :message="'Hello world!'">
      <template slot-scope="slotProps">
        Level: {{ slotProps.level }}, 内容: {{ slotProps.message }}
      </template>
    </anchored-heading>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    Vue.component('anchored-heading', {
      render: function (createElement) {
        return createElement('h' + this.level, [createElement('span', this.$scopedSlots.default({
          level: this.level,
          message: this.message
        }))])
      },
      props: ['level', 'message']
    })
    // 创建Vue实例(父组件)
    var app = new Vue({
      el: "#app",
      data: {}
    });
  </script>
</body>
  • 节点、树以及虚拟 DOM

在深入渲染函数之前,了解一些浏览器的工作原理是很重要的。以下面这段 HTML 为例:

html 复制代码
<div>
  <h1>My title</h1>
  Some text content
  <!-- TODO: Add tagline -->
</div>

当浏览器读到这些代码时,它会建立一个"DOM 节点"树来保持追踪所有内容,如同你会画一张家谱树来追踪家庭成员的发展一样。DOM 节点树如下图所示:

每个元素都是一个节点。每段文字也是一个节点。甚至注释也都是节点。

高效地更新所有这些节点会是比较困难的,不过所幸你不必手动完成这个工作。你只需要告诉 Vue 你希望页面上的 HTML 是什么,这可以是在一个模板里:

html 复制代码
<h1>{{ blogTitle }}</h1>

或者一个渲染函数里:

js 复制代码
render: function (createElement) {
  return createElement('h1', this.blogTitle)
}

createElement其实不是一个实际的 DOM 元素。它更准确的名字可能是 createNodeDescription,因为它所包含的信息会告诉 Vue 页面上需要渲染什么样的节点,包括及其子节点的描述信息。我们把这样的节点描述为"虚拟节点 (virtual node)",也常简写它为"VNode"。"虚拟 DOM "是我们对由 Vue 组件树建立起来的整个 VNode 树的称呼。

渲染函数的核心是创建虚拟 DOMVNode ),Vue 会将虚拟 DOM 转换为真实 DOM

只要在原生的 JavaScript 中可以轻松完成的操作,Vue 的渲染函数就不会提供专有的替代方法。比如,在模板中使用的 v-ifv-for

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <ul v-if="items.length">
      <li v-for="item in items">{{ item.name }}</li>
    </ul>
    <p v-else>No items found.</p>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 创建Vue实例(父组件)
    var app = new Vue({
      el: "#app",
      data: {
        items:[]
      }
    });
  </script>
</body>

这些都可以在渲染函数中用 JavaScriptif/elsemap 来重写:

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <my-component :items="items"></my-component>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>

    Vue.component('my-component', {
      props: ['items'],
      render: function (createElement) {
        if (this.items.length) {
          return createElement('ul', this.items.map(function (item) {
            return createElement('li', item.name)
          }))
        } else {
          return createElement('p', 'No items found.');
        }
      }
    })
    // 创建Vue实例(父组件)
    var app = new Vue({
      el: "#app",
      data: {
        items: [{'name': 'shan'}]
      }
    });
  </script>
</body>

渲染函数中没有与 v-model 的直接对应------你必须自己实现相应的逻辑:

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <my-component v-model="message"></my-component>
    <p>{{ message }}</p>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>

    Vue.component('my-component', {
      props: ['value'], // 接收父组件传入的值
      render: function (createElement) {
        const self = this; // 保存this引用,避免在回调中丢失

        return createElement('input', {
          // 绑定value属性
          attrs: {
            type: 'text',
            value: self.value // 从props接收值
          },
          // 监听input事件
          on: {
            input: function (event) {
              // 触发input事件,将新值传递给父组件
              self.$emit('input', event.target.value);
            }
          }
      })
    }
  })
    // 创建Vue实例(父组件)
    var app = new Vue({
      el: "#app",
      data: {
        message: 'Hello Vue!'
      }
    });
  </script>
</body>

这就是深入底层的代价,但与 v-model 相比,这可以让你更好地控制交互细节。

createElement 参数

createElement 函数接收三个参数:

  • 标签名称:可以是 HTML 标签名、组件选项对象或异步组件
  • 数据对象:描述节点的属性、事件等(可选)。若第二个参数是对象,视为数据对象(无显式子节点),若第二个参数是字符串 / 数组,视为子节点(无数据对象)
  • 子节点:子节点数组(可选)
javascript 复制代码
createElement(tag, data, children)

一个简单的渲染函数示例:

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <my-component></my-component>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    Vue.component('my-component', {
      render: function (createElement) {
        // createElement 是用于创建 VNode 的函数
        return createElement(
          'div',  // 标签名称
          // 数据对象
          {
            // 标签属性(可选)
            class: 'my-class',
            style: {
              color: 'red',
              fontSize: '14px'
            },
            attrs: {
              id: 'my-id'
            },
            on: {
              click: this.handleClick
            }
          },
          [
            // 子节点数组(可选)
            createElement('h1', 'Hello World'),
            createElement('p', '这是一个渲染函数示例')
          ]
        )
      },
      methods: {
        handleClick() {
          console.log('元素被点击了')
        }
      }
    })
    // 创建Vue实例(父组件)
    var app = new Vue({
      el: "#app",
      data: {}
    });

  </script>
</body>
深入数据对象

数据对象(第二个参数)用于描述节点的各种属性、事件、样式等信息。它是一个 JavaScript 对象,包含多个可选的键,用于精确控制 VNode 的行为。

json 复制代码
{
	// 1. 类名
	class: {
		active: this.isActive, // 动态类名
		'static-class': true // 静态类名
	},

	// 2. 内联样式
	style: {
		color: this.isActive ? 'red' : 'black', // 动态样式
		fontSize: '16px', // 静态样式
		padding: '10px'
	},

	// 3. HTML 属性(非响应式)
	attrs: {
		id: 'demo-container',
		'data-type': 'render-demo'
	},

	// 4. 组件 props(响应式)
	props: {
		message: this.message
	},

	// 5. 事件监听器(组件内部使用)
	on: {
		click: this.handleClick,
		input: this.handleInput
	},

	// 6. 原生事件监听器(绑定到根元素)
	nativeOn: {
		mouseenter: () => console.log('鼠标进入'),
		mouseleave: () => console.log('鼠标离开')
	},

	// 7. 自定义指令
	directives: [{
		name: 'focus', // 指令名称
		value: true, // 指令值
		expression: 'true',
		arg: 'arg',
		modifiers: {
			modifier: true
		}
	}],

	// 8. 作用域插槽
	scopedSlots: {
		default: props => createElement('span', props.text)
	},
	//如果组件是其它组件的子组件,需为插槽指定名称
	slot: 'name-of-slot',
	// 9. 其他特殊属性
	key: 'demo-key', // 用于列表渲染的key
	ref: 'demoRef', // 引用标识
	refInFor: true // 标识在v-for中使用ref
}
  • class类名:可以是字符串、对象或数组,对象形式适合动态切换类名(键为类名,值为布尔值)。等效于模板中的 :class
  • style样式:可以是字符串、对象或数组,CSS 属性名可以用驼峰式(fontSize )或短横线式('font-size' ),等效于模板中的 :style
  • attrs属性:用于设置 HTML 原生属性。等效于模板中不带 : 的属性,比如iddata-* 等属性。
  • props通信:用于传递给子组件的 props
  • on事件:用于监听组件内部触发的事件。
  • nativeOn事件:仅用于自定义组件,监听组件根元素的原生事件。等效于模板中的 @event-name.native
  • directives指令:用于应用自定义指令,需要指定指令名称、值和修饰符等信息。
  • scopedSlots插槽:作用域插槽。
  • key属性:用于 Vue 的虚拟 DOM diff 算法,在列表渲染时特别重要,提高性能,等效于模板中的 :key
  • refrefInForref 用于标识元素或组件,可通过 this.$refs 访问;refInFor 标识在循环中使用的 ref,此时 this.$refs 会是一个数组。

不是所有属性都需要同时使用,根据实际需求选择。复杂逻辑建议使用 JSX 语法,让代码更易读。

子节点

子节点(第三个参数)用于定义当前节点的子元素或文本内容,它可以是字符串、数组或通过 createElement 创建的 VNode

  • 字符串(文本节点):直接传递字符串时,会被解析为文本节点。
html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <my-component></my-component>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    Vue.component('my-component', {
      render: function (createElement) {
        // createElement 是用于创建 VNode 的函数
        return createElement(
          'div',  // 标签名称
          "这是一段文本"
        )
      }
    })
    // 创建Vue实例(父组件)
    var app = new Vue({
      el: "#app",
      data: {}
    });
  </script>
</body>

渲染结果:<p>这是一段文本</p>

  • VNode 对象(通过 createElement 创建):子节点可以是另一个 createElement 调用的返回值(虚拟 DOM 节点),用于嵌套元素。
html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <my-component></my-component>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    Vue.component('my-component', {
      render: function (createElement) {
        // createElement 是用于创建 VNode 的函数
        return createElement(
          'div',  // 标签名称
          [
            // 子节点数组(可选)
            createElement('span',"嵌套的 span")
          ]
        )
      }
    })
    // 创建Vue实例(父组件)
    var app = new Vue({
      el: "#app",
      data: {}
    });
  </script>
</body>

渲染结果:<div><span>嵌套的 span</span></div>

  • 数组(多个子节点):当需要多个子节点时,传递数组,数组元素可以是字符串、VNode 或其他合法类型。
html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <my-component></my-component>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    Vue.component('my-component', {
      render: function (createElement) {
        // createElement 是用于创建 VNode 的函数
        return createElement(
          'div',  // 标签名称
          [
            // 子节点数组(可选)
            createElement('h1', 'Hello World'),
            createElement('p', '这是一个渲染函数示例')
          ]
        )
      }
    })
    // 创建Vue实例(父组件)
    var app = new Vue({
      el: "#app",
      data: {}
    });
  </script>
</body>
  • 组件 VNode :子节点也可以是组件,通过 createElement(组件名) 创建。
html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <my-component></my-component>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    Vue.component('component-a', {
      template: '<h1>这是组件A</h1>'
    })
    Vue.component('my-component', {
      render: function (createElement) {
        // createElement 是用于创建 VNode 的函数
        return createElement(
          'div',  // 标签名称
          [
            // 子节点数组(可选)
            createElement('component-a')
          ]
        )
      }
    })
    // 创建Vue实例(父组件)
    var app = new Vue({
      el: "#app",
      data: {}
    });
  </script>
</body>

同一个 VNode 实例不能被重复用作不同位置的子节点,组件树中的所有 VNode 必须是唯一的。这意味着,下面的渲染函数是不合法的:

javascript 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <my-component></my-component>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>

    Vue.component('my-component', {
      render: function (createElement) {
        // 错误:创建一个 VNode 并重复使用
        const sameVNode = createElement('span', '重复节点')
        // createElement 是用于创建 VNode 的函数
        return createElement(
          'div',  // 标签名称
          [
            // 子节点数组(可选)
            sameVNode, sameVNode
          ]
        )
      }
    })
    // 创建Vue实例(父组件)
    var app = new Vue({
      el: "#app",
      data: {}
    });
  </script>
</body>
事件 & 按键修饰符

对于 .passive.capture.once 这些事件修饰符,确实可以通过特定的事件名前缀来实现,无需手动编写复杂逻辑。这些前缀直接加在事件名前,用于声明对应的修饰符行为:

修饰符 渲染函数中的事件名前缀 作用说明
.passive & 表示事件是被动的(用于触摸 / 滚动事件,提升性能,避免 preventDefault()
.capture ! 表示事件在捕获阶段触发(而非冒泡阶段)
.once ~ 表示事件只触发一次
组合修饰符 前缀可组合使用 例如 ~! 表示 .once.capture
html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <my-component></my-component>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>

    Vue.component('my-component', {
      render: function (createElement) {
        return createElement('div', {
          // 事件处理函数
          on: {
            // 等价于 @scroll.passive
            '&scroll': function (e) {
              console.log('滚动事件(passive模式)')
            },
            // 等价于 @click.capture
            '!click': function (e) {
              console.log('点击事件(捕获阶段触发)')
            },
            // 等价于 @click.once
            '~click': function () {
              console.log('点击事件(只触发一次)')
            },
            // 等价于 @click.once.capture
            '~!click': function () {
              console.log('点击事件(捕获阶段,只触发一次)')
            }
          }
        }, '这是一个div')
      }
    })
    // 创建Vue实例(父组件)
    var app = new Vue({
      el: "#app",
      data: {
      }
    });
  </script>
</body>

事件和按键修饰符不能像模板中那样直接使用 @click.prevent 这种简写形式,需要通过对象的方式进行配置。

  • 事件修饰符处理

常见的事件修饰符(.stop, .prevent, .self 等)需要在事件配置中通过原始方法进行操作:

html 复制代码
<body>
  <div id="app">
    <my-component></my-component>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    Vue.component('my-component', {
      render: function (createElement) {
        return createElement('form', {
          on: {
            // 直接绑定事件处理函数,不使用嵌套的 modifiers 配置
            submit: function (event) {
              // 1. 手动阻止表单默认提交行为(浏览器原生 API,必生效)
              event.preventDefault();
              // 2. 可选:阻止事件冒泡(对应 .stop 修饰符)
              event.stopPropagation();
              // 3. 模拟 .self 修饰符的效果
              // 当两者不相等时,说明事件是从子元素冒泡来的,直接返回不执行后续逻辑
              if (event.target !== event.currentTarget) return
              // 4. 自定义逻辑
              console.log('表单提交了(未刷新)');
            }
          }
        }, [
          // 显式声明按钮类型为 submit(确保触发表单 submit 事件)
          createElement('button', {
            attrs: { type: 'submit' }
          }, '提交表单')
        ]);
      }
    });

    var app = new Vue({
      el: "#app"
    });
  </script>
</body>
  • 按键修饰符处理

按键修饰符(.enter, .tab, .esc 等)需要在事件处理函数中手动判断按键码:

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <my-component></my-component>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>

    Vue.component('my-component', {
      render: function (createElement) {
        return createElement('input', {
          attrs: { type: 'text' },
          // 事件处理函数
          on: {
            keyup: function (event) {
              // 模拟 .enter 修饰符
              if (event.keyCode === 13) {
                console.log('回车键被按下')
              }

              // 模拟 .esc 修饰符
              if (event.keyCode === 27) {
                console.log('ESC键被按下')
              }
            }
          }
        })
      }
    })
    // 创建Vue实例(父组件)
    var app = new Vue({
      el: "#app",
      data: {
      }
    });
  </script>
</body>
  • 系统修饰符处理

系统修饰符(.ctrl, .alt, .shift, .meta)需要判断事件对象的相应属性:

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <my-component></my-component>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>

    Vue.component('my-component', {
      render: function (createElement) {
        return createElement('button', {
          // 事件处理函数
          on: {
            click: function (event) {
              // 模拟 .ctrl 修饰符
              if (event.ctrlKey) {
                console.log('Ctrl键+点击')
              }

              // 模拟 .alt 修饰符
              if (event.altKey) {
                console.log('Alt键+点击')
              }
            }
          }
        }, "这是一个点击按钮")
      }
    })
    // 创建Vue实例(父组件)
    var app = new Vue({
      el: "#app",
      data: {
      }
    });
  </script>
</body>
JSX

JSX 是一种JavaScript 的语法扩展,全称为 JavaScript XML ,它允许在 JavaScript代码中直接编写类似HTML 的标签结构JSX 最初由 React 引入,现在也被 VuePreact 等多个框架支持,用于更直观地描述 UI 结构。

Vue 2 中使用 JSX 需要借助 Babel 插件进行编译处理,最常用的是 @vue/babel-preset-jsx

shell 复制代码
npm install @vue/babel-preset-jsx @vue/babel-helper-vue-jsx-merge-props --save-dev

使用 JSX 作为渲染函数:

javascript 复制代码
// 注册全局组件
Vue.component('jsx-component', {
  props: ['message'],
  // JSX 直接写在 render 函数中
  render() {
    return (
      <div class="jsx-example">
        <h1>{this.message}</h1>
        <p>当前时间: {new Date().toLocaleString()}</p>
      </div>
    );
  }
});
函数式组件

Vue2 中,函数式组件是一种无状态、无实例的轻量级组件,它没有 this 上下文,也没有生命周期钩子,仅通过函数接收 propscontext 并返回虚拟节点(VNode)。这种组件性能更高,适合用于纯展示、逻辑简单的场景。

javascript 复制代码
Vue.component('my-component', {
  functional: true,
  // Props 是可选的
  props: {
    // ...
  },
  // 为了弥补缺少的实例
  // 提供第二个参数作为上下文
  render: function (createElement, context) {
    // ...
  }
})

在 2.3.0 之前的版本中,如果一个函数式组件想要接收 prop,则 props 选项是必须的。在 2.3.0 或以上的版本中,你可以省略 props 选项,所有组件上的属性都会被自动隐式解析为 prop

如果你使用了单文件组件,那么基于模板的函数式组件可以这样声明:

html 复制代码
<template functional>
</template>

更详细的通过渲染函数创建,示例代码如下:

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <anchored-heading :message="message" :list="items"  @item-click="handleItemClick"></anchored-heading>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    Vue.component('anchored-heading', {
      functional: true,
      // 接收的 props
      props: ['message', 'list'],
      render: function (createElement, context) {
        // context 包含:props、children、slots、data、parent、listeners 等
        const { props, listeners, slots } = context;
        // 使用 props 数据
        return createElement('p', [
          // 使用 props 数据
          createElement('p', `消息:${props.message}`),

          // 渲染列表
          createElement('ul',
            props.list.map((item, index) =>
              createElement('li', {
                // 绑定事件(通过 listeners 访问父组件传递的事件)
                on: {
                  click: () => listeners.itemClick(index, item)
                }
              }, item)
            )
          ),

          // 渲染插槽内容
          createElement('div', { style: { marginTop: '10px' } }, slots().default)
        ])
      }
    })
    // 创建Vue实例(父组件)
    var app = new Vue({
      el: "#app",
      data: {
        message: 'Hello world! 函数组件',
        items: ['A', 'B', 'C']
      },
      methods: {
        handleItemClick(index, item) {
          console.log(`点击了第 ${index} 项:${item}`);
        }
      }
    });
  </script>
</body>

组件需要的一切都是通过 context 参数传递,参数详解:

  • props:接收的 props 数据(context.props
  • children:子节点数组(原始 VNode,不推荐直接使用)
  • slots():返回插槽对象(slots().default 是默认插槽)
  • scopedSlots:(2.6.0+) 一个暴露传入的作用域插槽的对象。也以函数形式暴露普通插槽。
  • listeners:父组件传递的事件监听器(listeners.click 对应 @click
  • data:父组件传递的属性(如 classstyle 等)
  • parent:父组件实例
  • listeners:(2.3.0+) 一个包含了所有父组件为当前组件注册的事件监听器的对象。这是 data.on 的一个别名。
  • injections:(2.3.0+) 如果使用了 inject 选项,则该对象包含了应当被注入的属性。

在添加 functional: true 之后,需要更新我们的锚点标题组件的渲染函数,为其增加 context 参数,并将 this.$slots.default 更新为 context.children,然后将 this.level 更新为 context.props.level

通过向 createElement 传入 context.data 作为第二个参数,我们就把父组件上面所有的属性和事件监听器都传递下去了。事实上这是非常透明的,以至于那些事件甚至并不要求 .native 修饰符。

javascript 复制代码
Vue.component('my-functional-button', {
  functional: true,
  render: function (createElement, context) {
    // 完全透传任何 attribute、事件监听器、子节点等。
    return createElement('button', context.data, context.children)
  }
})
  • slots() 和 children 对比
html 复制代码
<my-functional-component>
  <p>默认内容</p>
  <template slot="footer">
    <button>底部按钮</button>
  </template>
</my-functional-component>
javascript 复制代码
// children 包含所有子节点(不区分插槽)
console.log(context.children); 
// 输出:[p标签的VNode, template标签的VNode]

console.log(slots());
// 输出:
{
  default: [p标签的VNode], // 默认插槽内容
  footer: [button标签的VNode] // 具名插槽footer的内容
}

对于这个组件,children 会给你两个段落标签,而 slots().default 只会传递第二个匿名段落标签,slots().foo 会传递第一个具名段落标签。同时拥有 childrenslots(),因此你可以选择让组件感知某个插槽机制,还是简单地通过传递 children,移交给其它组件去处理。

模板编译

Vue 的模板实际上被编译成了渲染函数。这是一个实现细节,通常不需要关心。但如果你想看看模板的功能具体是怎样被编译的,可能会发现会非常有意思。下面是一个使用 Vue.compile 来实时编译模板字符串的简单示例:

html 复制代码
  <div>
    <header>
      <h1>I'm a template!</h1>
    </header>
    <p v-if="message">{{ message }}</p>
    <p v-else>No message.</p>
  </div>

render:

javascript 复制代码
function anonymous(
) {
  with(this){return _c('div',[_m(0),(message)?_c('p',[_v(_s(message))]):_c('p',[_v("No message.")])])}
}

staticRenderFns:

javascript 复制代码
_m(0): function anonymous(
) {
  with(this){return _c('header',[_c('h1',[_v("I'm a template!")])])}
}

插件

插件通常用来为 Vue 添加全局功能。Vue 插件本质是一个包含 install 方法的对象(或函数),用于扩展 Vue 的功能,而插件的实现既可以包含逻辑处理,也可以包含 HTML 模板相关的功能(如注册全局组件、指令等)。插件的功能范围没有严格的限制------一般有下面几种:

  1. 添加全局方法或者属性。如:vue-custom-element
  2. 添加全局资源:指令/过滤器/过渡等。如 vue-touch
  3. 通过全局混入来添加一些组件选项。如 vue-router
  4. 添加 Vue 实例方法,通过把它们添加到 Vue.prototype 上实现。
  5. 一个库,提供自己的 API ,同时提供上面提到的一个或多个功能。如 vue-router

Vue.use 会自动阻止多次注册相同插件,届时即使多次调用也只会注册一次该插件。

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <!-- 使用插件提供的全局组件 -->
    <my-component title="hello world">
    </my-component>
  </div>

  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    const MyPlugin = {
      install(Vue) {
        // 注册全局组件
        Vue.component('my-component', {
          props: ['title'],
          template: '<div>插件组件:{{title}}</div>'
        });

        // 注册全局指令
        Vue.directive('my-directive', { /* ... */ });
      }
    };
    // 3. 安装插件
    Vue.use(MyPlugin);
    // 创建Vue实例(父组件)
    var app = new Vue({
      el: "#app",
      data: {}
    });
  </script>
</body>

Vue.js 官方提供的一些插件 (例如 vue-router) 在检测到 Vue 是可访问的全局变量时会自动调用 Vue.use()。然而在像 CommonJS 这样的模块环境中,你应该始终显式地调用 Vue.use()

javascript 复制代码
// 用 Browserify 或 webpack 提供的 CommonJS 模块环境时
var Vue = require('vue')
var VueRouter = require('vue-router')

// 不要忘了调用此方法
Vue.use(VueRouter)

Vue.js 的插件应该暴露一个 install 方法。这个方法的第一个参数是 Vue 构造器,第二个参数是一个可选的选项对象,一个标准的 Vue 2 插件格式如下:

javascript 复制代码
const MyPlugin = {
  // install 方法是插件的核心,接收 Vue 构造函数和可选参数
  install(Vue, options) {
    // 1. 注册全局组件
    Vue.component('my-component', {
      template: '<div>{{ message }}</div>',
      data() { return { message: '这是插件组件' } }
    });

    // 2. 注册全局指令
    Vue.directive('my-directive', {
      inserted(el) {
        el.style.color = 'red'; // 为元素设置红色文字
      }
    });

    // 3. 添加实例方法(通过原型链)
    Vue.prototype.$myMethod = function (value) {
      console.log('插件方法调用:', value);
    };

    // 4. 使用选项参数(安装时传入)
    if (options && options.globalProperty) {
      Vue.prototype[options.globalProperty] = '全局属性值';
    }
  }
};

// 安装插件(可传入可选参数)
Vue.use(MyPlugin, { globalProperty: '$customProp' });

Vue2插件和组件有什么区别?

  • 插件(Plugin) :扩展 Vue 框架功能,提供全局能力(如注册组件、指令等)功能更宽泛,可包含多个组件、指令、工具函数等。
html 复制代码
// 必须通过 Vue.use() 安装:在创建 Vue 实例前执行,用于初始化插件功能。
import MyPlugin from './my-plugin';
Vue.use(MyPlugin); // 安装插件(可传配置参数)

<!-- 使用插件注册的全局组件 -->
<plugin-component></plugin-component>

<!-- 使用插件注册的全局指令 -->
<div v-plugin-directive></div>

// 调用插件添加的实例方法
this.$pluginMethod();

插件是 "工具" :用于增强 Vue 本身的能力,提供全局可用的功能或工具集。

  • 组件(Component) :封装 UI 结构、样式和局部逻辑,用于页面渲染,专注于单一 UI 功能(如按钮、弹窗、表格)。
html 复制代码
// 组件的使用方式:

export default {
  components: {
    'my-component': MyComponent // 局部注册
  }
}
Vue.component('my-component', MyComponent); // 全局注册
// 模板中使用
<my-component :prop="value" @event="handle"></my-component>

组件是 "零件" :用于搭建页面的具体 UI 元素,可复用、可组合。

过滤器

Vue 2 中,过滤器(Filter )是用于格式化文本数据的功能,可在模板插值表达式 (双花括号插值和)和 v-bind(版本2.1.0+ 开始支持) 中使用,用于对数据进行处理后再展示。过滤器不会不会修改原始数据,只影响数据的显示形式。

javascript 复制代码
<!-- 在双花括号中 -->
{{ message | capitalize }}

<!-- 在 `v-bind` 中 -->
<div v-bind:id="rawId | formatId"></div>

过滤器分为局部过滤器 (仅在当前组件可用)和全局过滤器(全应用可用)。

  • 局部过滤器

你可以在一个组件的选项中定义本地的过滤器:

html 复制代码
<body>
  <div id="app">
    <p>{{message | toUpperCase}}</p>
    <p>{{message | toLowerCase}}</p>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 创建Vue实例
    var app = new Vue({
      el: "#app",
      data: {
        message: 'hello World'
      },
      filters: {
        // 转为大写
        toUpperCase(value) {
          return value ? value.toUpperCase() : '';
        },
        toLowerCase(value){
          return value ? value.toLowerCase() : '';
        }
      }
    })
  </script>
</body>
  • 全局过滤器

或者在创建 Vue 实例之前全局定义过滤器:

html 复制代码
<body>
  <div id="app">
    <p>{{message | toUpperCase}}</p>
    <p>{{message | toLowerCase}}</p>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    Vue.filter('toUpperCase', function (value) {
      return value ? value.toUpperCase() : '';
    })
    Vue.filter('toLowerCase', function (value) {
      return value ? value.toLowerCase() : '';
    })
    // 创建Vue实例
    var app = new Vue({
      el: "#app",
      data: {
        message: 'hello World'
      }
    })
  </script>
</body>

在模板中通过 |(管道符)使用过滤器,支持链式调用(多个过滤器依次处理),语法为:{``{ 数据 | 过滤器名 | 过滤器名2...}}

html 复制代码
<body>
  <div id="app">
    <p>原始文本:{{ message }}</p>
    <p>处理后:{{ message | truncate | toUpperCase }}</p>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 创建Vue实例
    var app = new Vue({
      el: "#app",
      data: {
        message: 'hello vue filters example'
      },
      filters: {
        // 局部过滤器:截取字符串
        truncate(value) {
          if (!value) return '';
          return value.length > 5 ? value.slice(0, 5) + '...' : value;
        },
        // 局部过滤器:转为大写
        toUpperCase(value) {
          return value ? value.toUpperCase() : '';
        }
      }
    })
  </script>
</body>

过滤器是 JavaScript 函数,因此可以接收参数,语法为:{``{ 数据 | 过滤器名(参数1, 参数2) }}

html 复制代码
<body>
  <div id="app">
    <p>原始文本:{{ message }}</p>
    <p>处理后:{{ message | truncate(5) | toUpperCase }}</p>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 创建Vue实例
    var app = new Vue({
      el: "#app",
      data: {
        message: 'hello vue filters example'
      },
      filters: {
        // 局部过滤器:截取字符串
        truncate(value, length) {
          if (!value) return '';
          return value.length > length ? value.slice(0, length) + '...' : value;
        },
        // 局部过滤器:转为大写
        toUpperCase(value) {
          return value ? value.toUpperCase() : '';
        }
      }
    })
  </script>
</body>

其中 message 的值作为第一个参数,普通字符串 'length' 作为第二个参数,以此类推可以添加n个参数。

注:Vue 3 已移除过滤器功能,推荐用计算属性或方法替代。

相关推荐
嘻嘻嘻开心2 小时前
Java IO流
java·开发语言
JIngJaneIL2 小时前
基于java+ vue家庭理财管理系统(源码+数据库+文档)
java·开发语言·前端·数据库·vue.js·spring boot
hakesashou2 小时前
python 随机函数可以生成字符串吗
开发语言·python
GISer_Jing2 小时前
Taro跨端开发实战:JX首页实现_Trae SOLO构建
前端·javascript·aigc·taro
FakeOccupational2 小时前
【经济学】 基本面数据(Fundamental Data)之 美国劳动力报告&非农就业NFP + ADP + 美国劳动力参与率LFPR
开发语言·人工智能·python
huluang2 小时前
Word文档批注智能克隆系统的设计与实现
开发语言·c#·word
superman超哥2 小时前
仓颉设计哲学核心:零成本抽象的实现原理与深度实践
开发语言·仓颉编程语言·仓颉·零成本抽象·仓颉设计
山上三树3 小时前
柔性数组(C语言)
c语言·开发语言·柔性数组