渐进式JavaScript框架:Vue 组件

渐进式JavaScript框架:Vue 组件

前言

Vue2 组件是构建应用的基础单元,它允许你将重复的应用拆分为独立、可复用的代码片段。每个组件都有自己的逻辑、模板和样式,使代码更易于维护和扩展。

组件基础

这里有一个 Vue 组件的示例:

html 复制代码
<body>
  <div id="app">
    <!-- 使用组件 -->
    <button-counter></button-counter>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 定义全局组件
    Vue.component('button-counter', {
      data: function () {
        return { count: 0 }
      },
      template: '<button v-on:click="count++">点击次数:{{count}}</button>'
    })
    // 创建实例
    var app = new Vue({
      el: "#app",
      data: {},
      methods: {}
    })
  </script>
</body>

如图所示:

上述例子中是 <button-counter>是我们创建的组件。我们通过创建的 Vue 根实例中,把这个组件作为自定义元素来使用。

因为组件是可复用的 Vue 实例,所以它们与 new Vue 接收相同的选项,例如 datacomputedwatchmethods 以及生命周期钩子等。仅有的例外是像 el 这样根实例特有的选项。

  • 组件的复用

你可以将组件进行任意次数的复用:

html 复制代码
<body>
  <div id="app">
    <!-- 使用组件 -->
    <button-counter></button-counter>
    <button-counter></button-counter>
    <button-counter></button-counter>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 定义全局主键
    Vue.component('button-counter', {
      data: function () {
        return { count: 0 }
      },
      template: '<button v-on:click="count++">点击次数:{{count}}</button>'
    })
    // 创建实例
    var app = new Vue({
      el: "#app",
      data: {},
      methods: {}
    })
  </script>
</body>

如图所示:

注意当点击按钮时,每个组件都会各自独立维护它的 count。因为你每用一次组件,就会有一个它的新实例被创建。

组件定义必须先于实例创建:确保在 new Vue() 之前完成组件注册。

  • 组件的data 必须是一个函数

当我们定义这个 <button-counter> 组件时,你可能会发现它的 data 并不是像这样直接提供一个对象:

js 复制代码
    Vue.component('button-counter', {
      data: { 
      	count: 0 
      },
      template: '<button v-on:click="count++">点击次数:{{count}}</button>'
    })

取而代之的是,一个组件的 data 选项必须是一个函数,因此每个实例可以维护一份被返回对象的独立的拷贝:

js 复制代码
    // 定义主键
    Vue.component('button-counter', {
      data: function () {
        return { count: 0 }
      },
      template: '<button v-on:click="count++">点击次数:{{count}}</button>'
    })

如果 Vue 没有这条规则,点击一个按钮就可能会影响到其它所有实例。

组件注册

通常一个应用会以一棵嵌套的组件树的形式来组织:

例如,你可能会有页头、侧边栏、内容区等组件,每个组件又包含了其它的像导航链接、博文之类的组件。

组件名

在注册一个组件的时候,我们始终需要给它一个名字。比如在全局注册的时候我们已经看到了:

js 复制代码
Vue.component('my-component-name', {
  // ... options ...
})

Vue.component 的第一个参数就是组件名 。

当直接在 DOM 中使用一个组件 (而不是在字符串模板或单文件组件) 的时候,我们强烈推荐遵循 W3C 规范中的自定义组件名 (字母全小写且必须包含一个连字符)。这会帮助你避免和当前以及未来的 HTML 元素相冲突。

定义组件名的方式有两种:

  1. 使用 kebab-case
js 复制代码
Vue.component('my-component-name', { /* ... */ })

当使用 kebab-case (短横线分隔命名) 定义一个组件时,你也必须在引用这个自定义元素时使用 kebab-case ,例如 <my-component-name>

  1. 使用 PascalCase
js 复制代码
Vue.component('MyComponentName', { /* ... */ })

当使用 PascalCase (首字母大写命名) 定义一个组件时,你在引用这个自定义元素时两种命名法都可以使用。也就是说 <my-component-name><MyComponentName> 都是可接受的。

对于绝大多数项目来说,在单文件组件和字符串模板中组件名应该总是 PascalCase 的------但是在 DOM 模板中总是 kebab-case 的。

复制代码
<!-- 在单文件组件和字符串模板中 -->
<MyComponent/>
<!-- 在 DOM 模板中 或者 在所有地方-->
<my-component></my-component>

组件名应该始终是多个单词的,根组件 App 以及 <transition><component> 之类的 Vue 内置组件除外。

这样做可以避免跟现有的以及未来的 HTML 元素相冲突,因为所有的 HTML 元素名称都是单个单词的。

为了能在模板中使用,这些组件必须先注册以便 Vue 能够识别。这里有两种组件的注册类型:全局注册局部注册

全局注册

至此,我们的组件都只是通过 Vue.component 全局注册的:

js 复制代码
Vue.component('my-component-name', {
  // ... options ...
})

全局注册 的组件可以用在其被注册之后的任何 (通过 new Vue) 新创建的 Vue 根实例,也包括其组件树中的所有子组件的模板中,文章开头介绍的写法算是全局注册的一种,你也可以把它写入单独JS中公用:

js 复制代码
// component.js
// 全局组件
Vue.component('button-counter', {
    data: function () {
        return { count: 0 }
    },
    template: '<button v-on:click="count++">点击次数:{{count}}</button>'
})

然后再HTML 页面中引入 Vue 库和组件注册代码:

html 复制代码
<body>
  <div id="app">
    <!-- 使用组件 -->
    <button-counter></button-counter>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script src="./component.js"></script>
  <script>
    // 创建实例
    var app = new Vue({
      el: "#app",
      data: {},
      methods: {}
    })
  </script>
</body>

全局注册的行为必须在根 Vue 实例 (通过 new Vue) 创建之前发生。

应用特定样式和约定的基础组件 (也就是展示类的、无逻辑的或无状态的组件) 应该全部以一个特定的前缀开头,比如 BaseAppV

复制代码
components/
|- BaseButton.vue
|- BaseTable.vue
|- BaseIcon.vue

只应该拥有单个活跃实例的组件应该以 The 前缀命名,以示其唯一性。这不意味着组件只可用于一个单页面,而是每个页面只使用一次。这些组件永远不接受任何 prop,因为它们是为你的应用定制的,而不是它们在你的应用中的上下文。

复制代码
components/
|- TheHeading.vue
|- TheSidebar.vue

和父组件紧密耦合的子组件应该以父组件名作为前缀命名。如果一个组件只在某个父组件的场景下有意义,这层关系应该体现在其名字上。因为编辑器通常会按字母顺序组织文件,所以这样做可以把相关联的文件排在一起。

复制代码
components/
|- SearchSidebar.vue
|- SearchSidebarNavigation.vue

组件名应该以高级别的 (通常是一般化描述的) 单词开头,以描述性的修饰词结尾。

复制代码
components/
|- SearchButtonClear.vue
|- SearchButtonRun.vue

局部注册

在组件内部通过 components 选项注册,仅在当前组件及其子组件中可用。

html 复制代码
<body>
  <div id="app">
    <!-- 使用组件 -->
    <button-counter></button-counter>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 定义局部组件
    const localComponent = {
      data: function () {
        return { count: 0 }
      },
      template: '<button v-on:click="count++">点击次数:{{count}}</button>'
    }
    // 创建实例
    var app = new Vue({
      el: "#app",
      data: {},
      methods: {},
      // 局部组件
      components: {
        'button-counter2': localComponent
      }
    })
  </script>
</body>

对于 components 对象中的每个属性来说,其属性名就是自定义组件名,其组件名值就是这个组件的选项对象。

根据组件使用频率和作用范围选择合适的注册方式,避免滥用全局注册。

在单文件组件、字符串模板和 JSX 中没有内容的组件应该是自闭合的------但在 DOM 模板里永远不要这样做。

复制代码
<!-- 在单文件组件、字符串模板和 JSX 中 -->
<MyComponent/>
<!-- 在 DOM 模板中 -->
<my-component></my-component>

模块系统

如果你使用了诸如 Babelwebpack 的模块系统。在这些情况下,我们推荐创建一个 components 目录,并将每个组件放置在其各自的文件中。

先创建两个子组件:

js 复制代码
// componentA.js
export default {
    template: "<button>componentA按钮</button>"
}
js 复制代码
// componentB.js
export default {
    template: "<button>componentB按钮</button>"
}

然后你需要在局部注册之前导入每个你想使用的组件:

javascript 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <component-a></component-a>
    <component-b></component-b>
  </div>
  <!-- <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script> -->
  <script type="module">
    import Vue from 'https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.esm.browser.js'
    import ComponentA from './componentA.js'
    import ComponentB from './componentB.js'

    // 创建实例
    var app = new Vue({
      el: "#app",
      data: {},
      methods: {},
      components: {
        ComponentA,
        ComponentB
      }
    })
  </script>
</body>

导入后的子组件便可以再父组件中使用。

使用ES6 的模板,所以访问时需要启动服务器,推荐使用VSCodeGo Live插件

组件通信

Vue2 中,组件通信是构建复杂应用的核心机制。由于 Vue2 采用单向数据流(父组件 → 子组件通过 props,子组件 → 父组件通过事件),组件间的通信需要根据层级关系选择合适的方式。

父传子:通过 Prop 向子组件传递数据

声明接收 props 有以下 3 种常见方式:

  • 简单数组形式(基础声明)

直接使用数组列出需要接收的 prop 名称,不指定类型和验证规则,适用于简单场景。

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <child-component message="hello,vue"></child-component>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 子组件
    Vue.component('child-component', {
      props: ['message','active','fontSize','fontColors'],
      template: '<div>{{message}}</div>'
    })
    // 创建实例
    var app = new Vue({
      el: "#app",
      data: {},
      methods: {}
    })
  </script>
</body>

最简洁,但无法限制传入数据的类型、必填性等。

  • 对象形式(带类型检查)

使用对象格式,为每个 prop 指定类型(type),可以进行基础的类型验证。

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
  	<!--在 HTML 中,直接写在标签上的属性值默认都是字符串类型(即使看起来像数字)-->
    <child-component message="hello,vue" font-size="100"></child-component>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 子组件
    Vue.component('child-component', {
      props: {
        'message': String,
        'active': Boolean,
        'fontSize': Number,
        'fontColors': Array,
	    'userInfo': Object,
        'callback': Function,
      },
      template: '<div>{{message}}</div>'
    })
    // 创建实例
    var app = new Vue({
      el: "#app",
      data: {},
      methods: {}
    })
  </script>
</body>

静态传递的props属性,默认都是字符串类型,除非使用v-bind:动态传递,才会自动进行类型检查,如果传入类型不匹配会在控制台报警告。

  • 详细配置形式(完整验证)

对每个 prop 进行详细配置,包括类型、必填性、默认值、自定义验证等,适用于复杂场景。

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <child-component message="hello,vue"></child-component>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 子组件
    Vue.component('child-component', {
      props: {
        'message': {
          type: String,
          required: true // 必须传入,否则报警告
        },
        'fontSize': {
          type: Number,
          default: 20 // 默认值(基础类型直接赋值)
        },
        // 对象类型,有默认值(需用函数返回)
        'userInfo': {
          type: Object,
          default: () => {  // 对象默认值(需用函数返回)
            return {  // 对象默认值
              name: 'default',
              age: 18
            }
          }
        },
        // 数组类型,有默认值(需用函数返回)
        'fontColors': {
          type: Array,
          default: () => [] // 空数组默认值
        },
        // 自定义验证规则
        'score': {
          type: Number,
          // 自定义验证函数,返回false则验证失败
          validator: (value) => {
            return value >= 0 && value <= 100 // 分数必须在0-100之间
          }
        }
      },
      template: '<div>{{message}}--{{fontSize}}</div>'
    })
    // 创建实例
    var app = new Vue({
      el: "#app",
      data: {},
      methods: {}
    })
  </script>
</body>

prop 验证失败的时候,(开发环境构建版本的) Vue 将会产生一个控制台的警告。

  • 类型检查

type属性可以是原生构造函数中的一个:StringNumberBooleanArrayObjectDateFunctionSymbol

type 还可以是一个自定义的构造函数,并且通过 instanceof 来进行检查确认。例如,给定下列现成的构造函数:

javascript 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <child-component :person="person"></child-component>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    function Person(firstName, lastName) {
      this.firstName = firstName;
      this.lastName = lastName;
    }
    // 子组件
    Vue.component('child-component', {
      props: {
        'person': {
          type: Object,  // 使用 Object 类型
          validator: (value) => {
            // 验证 prop 是否是 Person 实例
            return value instanceof Person;
          },
          required: true
        }
      },
      template: '<div>{{person.firstName}} - {{person.lastName}}</div>'
    })
    // 创建实例
    var app = new Vue({
      el: "#app",
      data: {
        'person': new Person('zhang', 'san')
      },
      methods: {}
    })
  </script>
</body>

在你提交的代码中,prop 的定义应该尽量详细,至少需要指定其类型。

  • Prop 的大小写

HTML 中的属性名是大小写不敏感的,所以浏览器会把所有大写字符解释为小写字符。这意味着当你使用 DOM 中的模板时,camelCase (驼峰命名法) 的 prop 名需要使用其等价的 kebab-case (短横线分隔命名) 命名:

js 复制代码
Vue.component('blog-post', {
  // 在 JavaScript 中是 camelCase 的
  props: ['postTitle'],
  template: '<h3>{{ postTitle }}</h3>'
})
html 复制代码
<!-- 在 HTML 中是 kebab-case 的 -->
<blog-post post-title="hello!"></blog-post>

如果你使用字符串模板,那么这个限制就不存在了。

在声明 prop 的时候,其命名应该始终使用 camelCase ,而在模板和 JSX 中应该始终使用 kebab-case

  • 在组件上使用 v-for

在自定义组件上,你可以像在任何普通元素上一样使用 v-for

html 复制代码
<my-component v-for="item in items" :key="item.id"></my-component>

当在组件上使用 v-for 时,key 是必须的。

然而,任何数据都不会被自动传递到组件里,因为组件有自己独立的作用域 。把迭代数据传递到组件里,我们要使用 prop

  • 传递静态或动态 Prop

你已经知道了可以像这样给 prop 传入一个静态的值:

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <child-component message="hello,vue"></child-component>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 子组件
    Vue.component('child-component', {
      props: ['message'],
      template: '<div>{{message}}</div>'
    })
    // 创建实例
    var app = new Vue({
      el: "#app",
      data: {},
      methods: {}
    })
  </script>
</body>

你也可以使用v-bind动态赋值:

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <child-component v-for="prop in prosts" :message="prop.message" :key="prop.id"></child-component>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 子组件
    Vue.component('child-component', {
      props: ['message'],
      template: '<div>{{message}}</div>'
    })
    // 创建实例
    var app = new Vue({
      el: "#app",
      data: {
        'prosts': [
          { id: 1, message: 'hello vue' },
          { id: 2, message: 'hello html' },
          { id: 3, message: 'hello js' }
        ]
      },
      methods: {}
    })
  </script>
</body>

在上述两个示例中,我们传入的值都是字符串类型的,但实际上任何类型的值都可以传给一个 prop

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <child-component v-for="prop in prosts" :message="prop.message" :active="prop.active" :font-size="prop.fontSize"
      :font-colors="prop.fontColors" :user-info="prop.userInfo" :key="prop.id"></child-component>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 子组件
    Vue.component('child-component', {
      props: {
        'message': String,
        'active': Boolean,
        'fontSize': Number,
        'fontColors': Array,
        'userInfo': Object
      },
      template: '<div>{{message}}---{{active}}------{{fontSize}}------{{fontColors}}------{{userInfo}}</div>'
    })
    // 创建实例
    var app = new Vue({
      el: "#app",
      data: {
        'prosts': [
          { id: 1, message: 'hello vue', active: true, fontSize: 1, fontColors: ['red', 'yellow'], userInfo: { name: 'zhangsan' } },
          { id: 2, message: 'hello html', active: true, fontSize: 2, fontColors: ['bule', 'black'], userInfo: { name: 'lisi' } },
          { id: 3, message: 'hello js', active: false, fontSize: 3, fontColors: ['green', 'white'], userInfo: { name: 'wangwu' } }
        ]
      },
      methods: {}
    })
  </script>
</body>

当组件变得越来越复杂的时候,为每个相关的信息定义一个 prop 会变得很麻烦:

所以是时候重构一下这个组件,让它变成接受一个单独的对象prop

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <child-component v-for="prop in prosts" :prop="prop" :key="prop.id"></child-component>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 子组件
    Vue.component('child-component', {
      props: {
        'prop': Object
      },
      template: '<div>{{prop.message}}---{{prop.active}}------{{prop.fontSize}}------{{prop.fontColors}}------{{prop.userInfo}}</div>'
    })
    // 创建实例
    var app = new Vue({
      el: "#app",
      data: {
        'prosts': [
          { id: 1, message: 'hello vue', active: true, fontSize: 1, fontColors: ['red', 'yellow'], userInfo: { name: 'zhangsan' } },
          { id: 2, message: 'hello html', active: true, fontSize: 2, fontColors: ['bule', 'black'], userInfo: { name: 'lisi' } },
          { id: 3, message: 'hello js', active: false, fontSize: 3, fontColors: ['green', 'white'], userInfo: { name: 'wangwu' } }
        ]
      },
      methods: {}
    })
  </script>
</body>

执行结果如图:

现在,不论何时为对象添加一个新的属性,它都可用在标签内。

它们在 IE 下并没有被支持,所以如果你需要在不 (经过 BabelTypeScript 之类的工具) 编译的情况下支持 IE,请使用折行转义字符取而代之。

  • 单向数据流

所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定 :父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外变更父级组件的状态,从而导致你的应用的数据流向难以理解。

额外的,每次父级组件发生变更时,子组件中所有的 prop 都将会刷新为最新的值。这意味着你不应该在一个子组件内部改变 prop。如果你这样做了,Vue 会在浏览器的控制台中发出警告。

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <child-component message="hello,vue"></child-component>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 子组件
    Vue.component('child-component', {
      props: ['message','active','fontSize','fontColors'],
      template: '<div>{{message}}</div>',
      data: function(){
        return {
          message:"这是一个默认数据"
        }
      }
    })
    // 创建实例
    var app = new Vue({
      el: "#app",
      data: {},
      methods: {}
    })
  </script>
</body>

执行结构如图:

  • 单个根元素

你的模板最终会包含的东西远不止一个标签:

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <child-component v-for="item in prosts" :key="item.id" 
      :message="item.message" 
      :name="item.name" >
    </child-component>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 子组件
    Vue.component('child-component', {
      props: ['message', 'name', 'age', 'address', 'sex', 'tel'],
      template: '<p>{{name}}</p><div>{{message}}</div>'
    })
    // 创建实例
    var app = new Vue({
      el: "#app",
      data: {
        'prosts': [
          { id: 1, message: 'hello vue', name: 'zhangsan'},
          { id: 2, message: 'hello html', name: 'lisi'},
          { id: 3, message: 'hello js', name: 'wangwu'}
        ]
      },
      methods: {}
    })
  </script>
</body>

然而如果你在模板中尝试这样写,Vue 会显示一个错误,并解释道 every component must have a single root element (每个组件必须只有一个根元素)。

你可以将模板的内容包裹在一个父元素内,来修复这个问题,例如:

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <child-component v-for="item in prosts" :key="item.id" 
      :message="item.message" 
      :name="item.name" >
    </child-component>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 子组件
    Vue.component('child-component', {
      props: ['message', 'name', 'age', 'address', 'sex', 'tel'],
      template: `
        <div class="child_lable">
          <p>{{name}}</p>
          <div>{{message}}</div>
        </div>
      `
    })
    // 创建实例
    var app = new Vue({
      el: "#app",
      data: {
        'prosts': [
          { id: 1, message: 'hello vue', name: 'zhangsan'},
          { id: 2, message: 'hello html', name: 'lisi'},
          { id: 3, message: 'hello js', name: 'wangwu'}
        ]
      },
      methods: {}
    })
  </script>
</body>
  • 非Prop的属性

一个非 prop 定义的 属性传向一个组件,不总能预见组件会被用于怎样的场景,而这些属性会被添加到这个组件的根元素上。

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <child-component type="text" class="date-picker-theme-dark"></child-component>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 子组件
    Vue.component('child-component', {
      props: {
        'message': String
      },
      template: '<input type="date" class="form-control">'
    })
    // 创建实例
    var app = new Vue({
      el: "#app",
      data: {},
      methods: {}
    })
  </script>
</body>

执行结果如图:

对于绝大多数属性来说,从外部提供给组件的值会替换掉组件内部设置好的值。所以如果传入 type="text" 就会替换掉 type="date" 并把它破坏!庆幸的是,classstyle 属性会稍微智能一些,即两边的值会被合并起来,从而得到最终的值:form-control date-picker-theme-dark

这个模式允许你在使用基础组件的时候更像是使用原始的 HTML 元素,而不会担心哪个元素是真正的根元素:

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <form class="form-inline">
      <child-component class="date-picker-theme-dark" required></child-component>
      <button>提交</button>
  </div>

  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 子组件
    Vue.component('child-component', {
      props: {
        'message': String
      },
      template: '<input type="date" class="form-control">'
    })
    // 创建实例
    var app = new Vue({
      el: "#app",
      data: {},
      methods: {}
    })
  </script>
</body>

如果你不希望组件的根元素继承属性,你可以在组件的选项中设置 inheritAttrs: false。例如:

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <child-component type="text" class="date-picker-theme-dark"></child-component>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 子组件
    Vue.component('child-component', {
      inheritAttrs: false,
      props: {
        'message': String
      },
      template: '<input type="date" class="form-control">'
    })
    // 创建实例
    var app = new Vue({
      el: "#app",
      data: {},
      methods: {}
    })
  </script>
</body>

注意 inheritAttrs: false 选项不会影响 styleclass 的绑定。

子传父:监听子组件事件

有些功能可能要求我们和父级组件进行沟通。子组件通过 $emit 触发自定义事件向父组件传递数据:

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <child-component message="hello,vue" v-on:handle-click="handleClick"></child-component>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 子组件
    Vue.component('child-component', {
      props: ['message'],
      template: `
        <div class="child_lable">
          <p>{{message}}</p>
          <button @click="active">点击{{count}}</button>
        </div>
      `,
      data: function() {
        return {
          count: 0
        }
      },
      methods: {
        active: function() {
          // 触发父组件的自定义handleClick事件,传入参数
          this.$emit('handle-click', this.count++);
        }
      }
    })
    // 创建实例
    var app = new Vue({
      el: "#app",
      data: {},
      methods: {
        handleClick: function(count) {
          console.log(count);
        }
      }
    })
  </script>
</body>

上述代码通过v-on自定义事件 (注意:不能使用驼峰式命名 的事件名称),然后通过$emit函数向父组件的自定义事件传递数据。执行结果如图:

  • 事件名

不同于组件和 prop,事件名不存在任何自动化的大小写转换。而是触发的事件名需要完全匹配监听这个事件所用的名称。

举个例子,如果触发一个 camelCase 名字的事件:

js 复制代码
this.$emit('myEvent')

则监听这个名字的 kebab-case 版本是不会有任何效果的:

js 复制代码
<my-component v-on:my-event="doSomething"></my-component>

不同于组件和prop,事件名不会被用作一个 JavaScript 变量名或 属性名,所以就没有理由使用 camelCasePascalCase 了。并且 v-on 事件监听器在 DOM 模板中会被自动转换为全小写 (因为 HTML 是大小写不敏感的),所以 v-on:myEvent 将会变成 v-on:myevent------导致 myEvent 不可能被监听到。

因此,我们推荐你始终使用 kebab-case 的事件名。

  • 自定义组件的 v-model

自定义事件也可以用于创建支持 v-model 的自定义输入组件。当在组件上使用 v-model 时,Vue 会默认做两件事:

  1. 父组件向子组件传递一个名为 valueprop(默认方式)。
  2. 子组件通过触发 input 事件(this.$emit('input', 新值))向父组件传递数据
html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <child-component v-model="message"></child-component>
    <div>{{message}}</div>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 子组件
    Vue.component('child-component', {
      props: ['value'],
      template: `
        <div class="child_lable">
          <input type="text" v-bind:value="value" v-on:input="$emit('input', $event.target.value)">
        </div>
      `
    })
    // 创建实例
    var app = new Vue({
      el: "#app",
      data: {
        message: "hello,vue"
      },
      methods: {}
    })
  </script>
</body>

一个组件上的 v-model 默认会利用名为 valueprop 和名为 input 的事件,但是像单选框、复选框等类型的输入控件可能会将 value属性 用于不同的目的。model 选项可以用来避免这样的冲突:

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <child-component v-model="isActive"></child-component>
    <div>{{isActive}}</div>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 子组件
    Vue.component('child-component', {
      model: {
        prop: 'checked',  // 自定义 prop 名
        event: 'toggle'   // 自定义事件名
      },
      props: {
        checked: Boolean
      },
      template: `
        <div class="child_lable">
          <input type="checkbox" v-bind:checked=checked v-on:input="$emit('toggle', $event.target.checked)">
        </div>
      `
    })
    // 创建实例
    var app = new Vue({
      el: "#app",
      data: {
        isActive: false
      },
      methods: {}
    })
  </script>
</body>

父组件的 v-model 会自动监听 toggle 事件,将事件传递的「新值」自动更新到 v-model 绑定的变量(即 isActive),上面的父组件代码等价于:

html 复制代码
<!-- 不使用 v-model 的等价写法 -->
<toggle-button 
  :checked="isActive"  <!-- 传递 prop -->
  @toggle="isActive = $event"  <!-- 监听自定义事件并更新值 -->
></toggle-button>
  • 将原生事件绑定到组件

要将原生事件绑定到组件上,需要使用 .native 修饰符。这是因为直接在组件上使用 v-on(或 @ 简写)默认监听的是组件内部触发的自定义事件,而非原生 DOM 事件。

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <child-component @click.native="handClick"></child-component>
    <div>{{isActive}}</div>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 子组件
    Vue.component('child-component', {
      model: {
        prop: 'checked',  // 自定义 prop 名
        event: 'toggle'   // 自定义事件名
      },
      props: {
        checked: Boolean
      },
      template: `
        <div class="child_lable">
          <input type="checkbox" v-bind:checked=checked v-on:input="$emit('toggle', $event.target.checked)">
        </div>
      `
    })
    // 创建实例
    var app = new Vue({
      el: "#app",
      data: {
        isActive: false
      },
      methods: {
        handClick() {
          console.log('父组件点击了子组件')
        }
      }
    })
  </script>
</body>

上述代码会给<child-component>的根元素绑定原生 click 事件。

如果子组件的根元素不是你想绑定事件的元素,示例代码如下:

js 复制代码
    Vue.component('child-component', {
      model: {
        prop: 'checked',  // 自定义 prop 名
        event: 'toggle'   // 自定义事件名
      },
      props: {
        checked: Boolean
      },
      template: `
        <div class="child_lable">
          <div>这是默认子组件内容</div>
          <input type="checkbox" v-bind:checked=checked v-on:input="$emit('toggle', $event.target.checked)">
        </div>
      `
    })

你想给内部的checkbox绑定事件,此时不会如你预期地被调用。解决办法一种就是前面介绍的通过$emit转发自定义事件,另一种使用使用 $listenersVue 2.4+)。

$listeners 是组件实例的一个属性,用于获取父组件传递给当前组件的所有事件监听器 (不包括 .native 修饰符的事件)。它的主要作用是实现事件透传

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <child-component @input="handleInput" @focus="handleFocus" @blur="handleBlur"></child-component>
    <div>{{isActive}}</div>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 子组件
    Vue.component('child-component', {
      template: `
        <div class="child_lable">
          <div>这是默认子组件内容</div>
          <input type="checkbox" v-on="$listeners">
        </div>
      `
    })
    // 创建实例
    var app = new Vue({
      el: "#app",
      data: {
        isActive: false
      },
      methods: {
        handleInput(event) {
          console.log('输入内容:', event.target.value)
        },
        handleFocus() {
          console.log('输入框获得焦点')
        },
        handleBlur() {
          console.log('输入框失去焦点')
        }
      }
    })
  </script>
</body>

如图所示:

通过 v-on="$listeners" 将父组件的所有事件监听器绑定到内部 <input> 上。在 <child-component> 内部,this.$listeners 会是一个包含这三个事件的对象,结构如下:

javascript 复制代码
{
  input: handleInput,   // 对应父组件的 handleInput 方法 key=事件名,value=回调函数
  focus: handleFocus,   // 对应父组件的 handleFocus 方法
  blur: handleBlur      // 对应父组件的 handleBlur 方法
}

$listeners 可以指定特定事件进行使用,而不必一次性绑定所有事件监听器。

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <child-component @input="handleInput" @focus="handleFocus" @blur="handleBlur"
      @click="handleClick"></child-component>
    <div>{{isActive}}</div>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 子组件
    Vue.component('child-component', {
      template: `
        <div class="child_lable">
          <div>这是默认子组件内容</div>
          <!-- 1. 只绑定 click 事件 -->
          <button @click="$listeners.click">只触发点击事件</button>

          <!-- 2. 只绑定 input 事件 -->
          <input 
            type="text" 
            v-on="$listeners"
            placeholder="只触发输入事件"
          >
        </div>
      `
    })
    // 创建实例
    var app = new Vue({
      el: "#app",
      data: {
        isActive: false
      },
      methods: {
        handleInput(event) {
          console.log('输入内容:', event.target.value)
        },
        handleFocus() {
          console.log('输入框获得焦点')
        },
        handleBlur() {
          console.log('输入框失去焦点')
        },
        handleClick() {
          console.log('点击子组件')
        }
      }
    })
  </script>
</body>

$attrsVue 中的一个特殊语法,用于将父组件传递的未被 props 声明的属性和事件监听器批量绑定到子组件的某个元素上。它主要用于组件封装时的属性透传,避免手动逐个绑定属性和事件。

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <child-component placeholder="只触发输入事件" disabled></child-component>
    <div>{{isActive}}</div>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 子组件
    Vue.component('child-component', {
      inheritAttrs: false,
      template: `
        <div class="child_lable">
          <div>这是默认子组件内容</div>
          <input v-bind="$attrs" type="text">
        </div>
      `
    })
    // 创建实例
    var app = new Vue({
      el: "#app",
      data: {},
      methods: {}
    })
  </script>
</body>

执行结果如图:

设置inheritAttrs: false根元素不会自动继承 $attrs 中的属性。通过 v-bind="$attrs"能大幅减少重复代码,让子组件自动支持原生元素的属性和事件,提升组件的灵活性和易用性。

  • .sync 修饰符

.sync 修饰符是 Vue 中用于实现父子组件双向数据绑定的语法糖,简化了父子组件间数据更新的通信流程。

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <child-component :title.sync="pageTitle"></child-component>
    <div>{{pageTitle}}</div>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 子组件
    Vue.component('child-component', {
      props: ['title'],
      template: `
        <div class="child_lable">
          <div>这是默认子组件内容</div>
          <input type="text" @input="changeHandle">
        </div>
      `,
      methods: {
        changeHandle(e) {
          this.$emit('update:title', e.target.value)
        }
      }
    })
    // 创建实例
    var app = new Vue({
      el: "#app",
      data: {
        pageTitle: "初始标题"
      },
      methods: {}
    })
  </script>
</body>

在使用 .sync 修饰符时,update: 前缀是必须的。.sync 本质是语法糖,会被 Vue 自动解析为:

html 复制代码
  <div id="app">
    <child-component :title="pageTitle" @update:title="pageTitle = $event"></child-component>
    <div>{{pageTitle}}</div>
  </div>

当我们用一个对象同时设置多个 prop 的时候,也可以将这个 .sync 修饰符和 v-bind 配合使用:

html 复制代码
  <div id="app">
    <child-component :title.sync="object"></child-component>
    <div>{{pageTitle}}</div>
  </div>

这样会把 对象中的每一个属性(如 title) 都作为一个独立的 prop 传进去,然后各自添加用于更新的 v-on 监听器。

插槽

Vue2 的插槽(Slot )是组件间内容分发的重要机制,允许父组件向子组件的指定位置插入内容,实现组件的灵活复用。

插槽本质上是子组件中预留的「内容占位符」,父组件可以在使用子组件时,将内容填充到这些占位符中。

默认插槽(匿名插槽)

默认插槽是最简单的插槽形式,子组件中用 <slot> 标签定义一个未命名的插槽,父组件传入的内容会默认填充到这个插槽中。

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <child-component message="hello,vue">
      <h1>这是父组件插入的内容</h1>
    </child-component>
    
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 子组件
    Vue.component('child-component', {
      props: ['message'],
      template: `
        <div class="child_lable">
          <div>{{message}}</div>
          <slot></slot><!-- 定义默认插槽,父组件内容会插入到这里 -->
        </div>
      `
    })
    // 创建实例
    var app = new Vue({
      el: "#app",
      data: {},
      methods: {}
    })
  </script>
</body>

当组件渲染的时候,<slot>标签将会被替换(插槽内可以包含任何模板代码,包括 HTML),执行结果如图:

如果父组件没有提供内容,插槽可以显示默认内容:

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <child-component message="hello,vue"></child-component>
    
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 子组件
    Vue.component('child-component', {
      props: ['message'],
      template: `
        <div class="child_lable">
          <div>{{message}}</div>
          <slot>这是默认内容</slot><!-- 定义默认插槽,父组件内容会插入到这里 -->
        </div>
      `
    })
    // 创建实例
    var app = new Vue({
      el: "#app",
      data: {},
      methods: {}
    })
  </script>
</body>

执行结果如图:

当父组件再<child-component>标签里面编写内容,会替换子组件中的 <slot> 标签位置。

具名插槽(Named Slots)

当子组件需要多个插槽时,可以给插槽命名(name 属性),父组件通过 <template slot="名称">v-slot:名称 指定内容对应的插槽。

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <child-component>
      <template slot="center">
        <h1>这是中间内容</h1>
      </template>
    </child-component>
    
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 子组件
    Vue.component('child-component', {
      props: [],
      template: `
      <div class="child_lable">
        <div class="header">
          <div>头部标签</div>
          <slot name="header">这是默认内容</slot>
        </div>
        <div class="center">
          <div>中间标签</div>
          <slot name="center">这是默认内容2</slot>
        </div>
        <div class="footer">
          <div>尾部标签</div>
          <slot name="footer">这是默认内容3</slot>
        </div>
      </div>
      `
    })
    // 创建实例
    var app = new Vue({
      el: "#app",
      data: {},
      methods: {}
    })
  </script>
</body>

执行结果如图:

Vue 2.6.0 引入了 v-slot 指令,替代 slot 属性,语法更统一:

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <child-component>
      <template v-slot:center>
        <h1>这是中间内容</h1>
      </template>
    </child-component>
    
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 子组件
    Vue.component('child-component', {
      props: [],
      template: `
      <div class="child_lable">
        <div class="header">
          <div>头部标签</div>
          <slot name="header">这是默认内容</slot>
        </div>
        <div class="center">
          <div>中间标签</div>
          <slot name="center">这是默认内容2</slot>
        </div>
        <div class="footer">
          <div>尾部标签</div>
          <slot name="footer">这是默认内容3</slot>
        </div>
      </div>
      `
    })
    // 创建实例
    var app = new Vue({
      el: "#app",
      data: {},
      methods: {}
    })
  </script>
</body>

v-slot:xxx 可以缩写为 #xxx

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <child-component>
      <template #center>
        <h1>这是中间内容</h1>
      </template>
    </child-component>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 子组件
    Vue.component('child-component', {
      props: [],
      template: `
      <div class="child_lable">
        <div class="header">
          <div>头部标签</div>
          <slot name="header">这是默认内容</slot>
        </div>
        <div class="center">
          <div>中间标签</div>
          <slot name="center">这是默认内容2</slot>
        </div>
        <div class="footer">
          <div>尾部标签</div>
          <slot name="footer">这是默认内容3</slot>
        </div>
      </div>
      `
    })
    // 创建实例
    var app = new Vue({
      el: "#app",
      data: {},
      methods: {}
    })
  </script>
</body>

作用域插槽(Scoped Slots)

作用域插槽允许子组件向父组件传递数据,父组件可以根据子组件传递的数据来定制插槽内容

默认方式,Vue 2.6.0 前语法,使用slot-scope属性,示例代码如下:

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <child-component>
      <!-- 使用 slot-scope 接收子组件传递的数据 -->
      <template slot-scope="item">
        <h1>{{item.message}}</h1>
      </template>
    </child-component>

  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 子组件
    Vue.component('child-component', {
      props: [],
      template: `
      <div class="child_lable">
        <slot :message="message"></slot>
      </div>
      `,
      data: function () {
        return {
          message: "这是一个默认的内容"
        }
      }
    })
    // 创建实例
    var app = new Vue({
      el: "#app",
      data: {},
      methods: {}
    })
  </script>
</body>

如果是具名插槽,示例代码如下:

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <child-component>
      <!-- 使用 slot-scope 接收子组件传递的数据 -->
      <template slot="content" slot-scope="item">
        <h1>{{item.message}}</h1>
      </template>
    </child-component>
    
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 子组件
    Vue.component('child-component', {
      props: [],
      template: `
      <div class="child_lable">
        <slot name="content" :message="message"></slot>
      </div>
      `,
      data:function(){
          return {
            message:"这是一个默认的内容"
          }
      }
    })
    // 创建实例
    var app = new Vue({
      el: "#app",
      data: {},
      methods: {}
    })
  </script>
</body>

Vue 2.6.0+ 新语法,使用v-slot属性,示例代码如下:

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <child-component>
      <template v-slot="item">
        <h1>{{item.message}}</h1>
      </template>
    </child-component>

  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 子组件
    Vue.component('child-component', {
      props: [],
      template: `
      <div class="child_lable">
        <slot :message="message"></slot>
      </div>
      `,
      data: function () {
        return {
          message: "这是一个默认的内容"
        }
      }
    })
    // 创建实例
    var app = new Vue({
      el: "#app",
      data: {},
      methods: {}
    })
  </script>
</body>

如果是具名插槽,需要将 v-slot: 写在插槽名称后:

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <child-component>
      <template v-slot:content="item">
        <h1>{{item.message}}</h1>
      </template>
    </child-component>
    
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 子组件
    Vue.component('child-component', {
      props: [],
      template: `
      <div class="child_lable">
        <slot name="content" :message="message"></slot>
      </div>
      `,
      data:function(){
          return {
            message:"这是一个默认的内容"
          }
      }
    })
    // 创建实例
    var app = new Vue({
      el: "#app",
      data: {},
      methods: {}
    })
  </script>
</body>

动态组件 & 异步组件

动态组件

动态组件是一种可以根据数据动态渲染不同组件的技术,通过 <component> 元素配合 is 属性实现。这在需要根据不同条件展示不同组件的场景中非常有用。

定义两个子组件,示例代码如下:

js 复制代码
// componentA.js
export default {
    template: `
        <div class="componentA" @click="handleClick">
            <a href="#">这是组件A</a>
            <span>{{active? "看见我":"看不见我"}}</span>
        </div>
    `,
    data:function (){
        return {
            active: false
        }
    },
    methods: {
        handleClick: function () {
            this.active = !this.active;
        }
    }
}
// componenB.js
export default {
    template: `
        <div class="componentB">
            这是组件B
        </div>
    `
}

再父组件中进行切换,示例代码如下:

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <!-- 动态组件:通过 is 属性指定要渲染的组件 -->
    <component :is="currentComponent"></component>

    <!-- 切换按钮 -->
    <button @click="currentComponent = 'ComponentA'">显示组件A</button>
    <button @click="currentComponent = 'ComponentB'">显示组件B</button>
  </div>
  <script type="module">
    import Vue from 'https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.esm.browser.js'
    import ComponentA from '../components/ComponentA.js'
    import ComponentB from '../components/ComponentB.js'
    // 创建实例
    var app = new Vue({
      el: "#app",
      data: {
        currentComponent: 'ComponentA'
      },
      methods: {},
      components: {
        ComponentA,
        ComponentB
      }
    })
  </script>
</body>

如图所示:

你会注意到,如果你点击组件A的内容切换后,然后再点击组件B,是不会继续展示你之前选择的内容。这是因为你每次切换新标签的时候,Vue 都创建了一个新的组件实例。

重新创建动态组件的行为通常是非常有用的,但是我们更希望那些标签的组件实例能够被在它们第一次被创建的时候缓存下来。为了解决这个问题,我们可以用一个 <keep-alive> 元素将其动态组件包裹起来。

<keep-alive> 是一个内置的抽象组件,用于缓存组件实例,避免组件在切换时被频繁创建和销毁,而是被缓存到内存中。

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <!-- 动态组件:通过 is 属性指定要渲染的组件 -->
    <keep-alive>
      <component :is="currentComponent"></component>
    </keep-alive>

    <!-- 切换按钮 -->
    <button @click="currentComponent = 'ComponentA'">显示组件A</button>
    <button @click="currentComponent = 'ComponentB'">显示组件B</button>
  </div>
  <script type="module">
    import Vue from 'https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.esm.browser.js'
    import ComponentA from './ComponentA.js'
    import ComponentB from './ComponentB.js'
    // 创建实例
    var app = new Vue({
      el: "#app",
      data: {
        currentComponent: 'ComponentA'
      },
      methods: {},
      components: {
        ComponentA,
        ComponentB
      }
    })
  </script>
</body>

现在点击组件A的内容切换后,再怎么来回切换,都会保持它上一次操作的状态。

is属性除了动态组件渲染,还可以解决 HTML 解析限制。HTML 中某些标签(如 <table><ul><select>)对直接嵌套自定义组件有限制(浏览器会忽略不合法的子元素)。

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <table border="1px solid black">
      <my-component v-for="item in items" :key="item.id" :message="item.message"></my-component>
    </table>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 子组件
    Vue.component('my-component', {
      props: ['message'],
      template: '<tr><td>{{message}}</td></tr>'
    })
    // 创建实例
    var app = new Vue({
      el: "#app",
      data: {
        'items': [
          { id: 1, message: 'hello vue' },
          { id: 2, message: 'hello html' },
          { id: 3, message: 'hello js' }
        ]
      },
      methods: {}
    })
  </script>
</body>

上述代码执行后,由于浏览器的容错机制将非法元素(如 <my-component>)移到 <table> 外部,导致未按预期将内容显示在边框中。

幸好这个特殊的 is 属性给了我们一个变通的办法:

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <table border="1px solid black">
       <tr is="my-component" v-for="item in items" :key="item.id" :message="item.message"></tr>
    </table>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 子组件
    Vue.component('my-component', {
      props: ['message'],
      template: '<tr><td>{{message}}</td></tr>'
    })
    // 创建实例
    var app = new Vue({
      el: "#app",
      data: {
        'items': [
          { id: 1, message: 'hello vue' },
          { id: 2, message: 'hello html' },
          { id: 3, message: 'hello js' }
        ]
      },
      methods: {}
    })
  </script>
</body>

需要注意的是如果我们从以下来源使用模板的话,这条限制是不存在的:

  • 字符串 (例如:template: '...')
  • 单文件组件 (.vue)
  • <script type="text/x-template">

异步组件

异步组件是一种按需加载组件的方式,它可以在需要时才加载组件的代码,从而减小初始打包体积,提升应用加载速度。

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <button @click="show = true">加载组件</button>
    <async-component v-if="show"></async-component>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 全局注册异步组件
    Vue.component('async-component', function (resolve, reject) {
      // 模拟异步加载(实际项目中可替换为Ajax请求组件定义)
      setTimeout(() => {
        // 假设从服务器加载的组件定义
        const componentDef = {
          template: '<div>这是异步加载的组件</div>',
          data() { return { msg: '加载成功' } }
        }
        resolve(componentDef); // 通知Vue组件已加载完成
      }, 1000);

    });
    // 创建实例
    var app = new Vue({
      el: "#app",
      data: { show: false },
      methods: {},
      components: {}
    })
  </script>
</body>

如你所见,这个工厂函数会收到一个 resolve 回调,这个回调函数会在你从服务器得到组件定义的时候被调用。你也可以调用 reject(reason) 来表示加载失败。这里的 setTimeout 是为了演示用的,如何获取组件取决于你自己。

一个推荐的做法是将异步组件和 webpackcode-splitting 功能一起配合使用:

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <button @click="show = true">加载组件</button>
    <async-component v-if="show"></async-component>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 全局注册异步组件
    Vue.component('async-component', function (resolve, reject) {
      // 会通过 Ajax 请求加载
      require(['../components/componentA.js'], resolve)
    });
    // 创建实例
    var app = new Vue({
      el: "#app",
      data: { show: false },
      methods: {},
      components: {}
    })
  </script>
</body>

你也可以返回一个 Promise 对象(ES6用法),这是更现代的写法:

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <button @click="show = true">加载组件</button>
    <async-component v-if="show"></async-component>
  </div>
  <script type="module">
    import Vue from 'https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.esm.browser.js'
    // 全局注册异步组件
    Vue.component('async-component', () => import('../components/componentA.js'));
    // 创建实例
    var app = new Vue({
      el: "#app",
      data: { show: false },
      methods: {},
      components: {}
    })
  </script>
</body>

当使用局部注册的时候,你也可以直接提供一个返回 Promise 的函数:

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <button @click="show = true">加载组件</button>
    <async-component v-if="show"></async-component>
  </div>
  <script type="module">
    import Vue from 'https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.esm.browser.js'
    // 创建实例
    var app = new Vue({
      el: "#app",
      data: { show: false },
      methods: {},
      components: {
        "async-component": () => import('../components/componentA.js')
      }
    })
  </script>
</body>

你还可以配置异步组件的加载状态和错误状态。

先创建加载中组件和加载失败组件,示例代码如下:

js 复制代码
//加载中组件 loadingComponent.js
export default {
    template: `
        <div class="loading">
            <p>加载中,请稍候...</p>
        </div>
    `
}
//加载失败后组件 erroComponent.js
export default {
    template: `
        <div class="error">
            <p>加载失败</p>
        </div>
    `
}

再页面中使用,示例代码如下:

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <button @click="show = true">加载组件</button>
    <async-component v-if="show"></async-component>
  </div>
  <script type="module">
    import Vue from 'https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.esm.browser.js'
    import LoadingComponent from './components/loadingComponent.js'
    import ErrorComponent from './components/erroComponent.js'

    const mockSlowComponent = new Promise((resolve) => {
      // 模拟2秒的加载时间,确保超过delay
      setTimeout(() => {
        resolve({
          template: '<div class="success">组件加载成功!</div>'
        });
      }, 2000);
    });
    const AsyncComponent = () => ({
      // 需要加载的组件 (应该是一个 Promise 对象)
      component: mockSlowComponent,
      // 加载过程中显示的组件
      loading: LoadingComponent,
      // 加载失败时显示的组件
      error: ErrorComponent,
      // 展示加载组件前的延迟时间,默认:200ms
      delay: 200,
      // 如果提供了超时时间且加载超时,
      // 则使用加载失败组件。默认:Infinity
      timeout: 3000
    })
    // 创建实例
    var app = new Vue({
      el: "#app",
      data: { show: false },
      methods: {},
      components: {
        "async-component": AsyncComponent
      }
    })
  </script>
</body>

组件之间的访问

Vue 开发中,"处理边界情况" 指的是处理那些常规组件通信和数据流之外的特殊场景。这些场景往往涉及到组件树深层嵌套、跨层级访问、动态组件特殊处理等情况。

在绝大多数情况下,我们最好不要触达另一个组件实例内部或手动操作 DOM 元素。不过也确实在一些情况下做这些事情是合适的。

访问根实例

在深层嵌套的组件中,有时需要访问根 Vue 实例(通过new Vue()创建的实例),可使用$root

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', {
      template: '<div @click="foo">{{$root.show}}</div>',
      methods: {
        foo() {
          // 访问根组件的计算属性
          console.log(this.$root.bar)
          // 调用根组件的方法
          this.$root.baz()
          // 写入根组件的数据
          this.$root.show = true
        }
      }
    })
        // 创建实例
    var app = new Vue({
      el: "#app",
      data: { show: false },
      methods: {
        baz() {
          console.log('baz')
        }
      },
      computed: {
        bar: function () { console.log('bar') }
      }
    })
  </script>
</body>

过度依赖$root会导致组件耦合性提高,大型应用建议使用 Vuex

访问父组件实例

子组件可通过$parent访问直接父组件(和 $root 类似),但不推荐用于生产环境,会导致组件间强耦合(谨慎使用):

html 复制代码
<body>
  <div id="app">
    <my-component>
      <child-component></child-component>
    </my-component>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 子组件
    Vue.component('child-component', {
      template: '<button @click="callParent">获取父组件</button>',
      methods: {
        callParent() {
          // 调用父组件的属性
          console.log(this.$parent.show) // 输出 false
          // 调用父组件的方法
          this.$parent.parentMethod() // 输出 父组件被调用
          // 写入父组件的属性
          this.$parent.show = true
          console.log(this.$parent.show) // 输出 true
        }
      }
    })
    // 父组件
    Vue.component('my-component', {
      template: '<div>父组件<slot></slot></div>',
      data: function () {
        return {
          show: false
        }
      },
      methods: {
        parentMethod() {
          console.log("父组件被调用")
        }
      }
    })
    // 创建实例
    var app = new Vue({
      el: "#app",
      data: {},
      methods: {},
      computed: {}
    })
  </script>
</body>

应该优先通过 prop 和事件进行父子组件之间的通信,而不是 this.$parent 或变更 prop

访问子组件实例

  • 使用 $children 访问子组件

$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('child-component', {
      template: '<div>这是子组件</div>',
      data: function () {
        return {
          show: false
        }
      },
      methods: {
        childMethod() {
          console.log("子组件方法被调用")

        }
      }
    })
    // 父组件
    Vue.component('my-component', {
      template: `
      <div>父组件
        <button @click="callChild">获取父组件</button>
        <child-component></child-component>
      </div>`,
      methods: {
        callChild() {
          // $children 是子组件实例数组
          console.log(this.$children) // 输出 [VueComponent]
          // 访问第一个子组件的数据
          console.log(this.$children[0].show) // 输出 false
          // 调用第一个子组件的方法
          this.$children[0].childMethod() // 输出 子组件方法被调用
          // 写入第一个子组件内容
          this.$children[0].show= true
          console.log(this.$children[0].show) // 输出 true
        }
      }
    })
    // 创建实例
    var app = new Vue({
      el: "#app",
      data: {},
      methods: {},
      computed: {}
    })
  </script>
</body>

$children 数组的顺序不固定,与子组件在模板中的渲染顺序无关,不能依赖索引访问特定子组件。由于顺序不确定,容易导致代码逻辑出错,且会增加组件间耦合度。

  • 使用 $refs 访问子组件(推荐)

$refs 是父组件实例的一个属性,它返回一个对象,键是子组件或 DOM 元素的 ref 属性值,值是对应的子组件实例或 DOM 元素。通过给子组件添加 ref 属性,父组件可以精确访问指定子组件。

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('child-component', {
      template: `
        <div>
          <div>计数:{{count}}</div>
          <input ref="myInput" type="text" />
        </div>
      `,
      data: function () {
        return {
          count: 0
        }
      },
      methods: {
        increment() {
          this.count++
        }
      }
    })
    // 父组件
    Vue.component('my-component', {
      template: `
      <div>父组件
        <button @click="callChild">获取父组件</button>
        <child-component ref="firstChild"></child-component>
        <child-component ref="secondChild"></child-component>
      </div>`,
      methods: {
        callChild() {
          // 访问子组件实例
          this.$refs.firstChild.increment(); // 调用第一个子组件的方法
          console.log(this.$refs.firstChild.count);
          console.log(this.$refs.secondChild.count); // 获取第二个子组件的数据
          // 访问DOM元素
          this.$refs.firstChild.$refs.myInput.focus(); // 让输入框获取焦点
        }
      }
    })
    // 创建实例
    var app = new Vue({
      el: "#app",
      data: {},
      methods: {},
      computed: {}
    })
  </script>
</body>

通过 ref 名称可以精确访问指定子组件,避免 $children 的无序问题。还可以绑定到普通 DOM 元素,便于操作 DOM

ref 绑定到 v-for 循环的子组件时,$refs 会返回一个数组,包含所有循环生成的子组件:

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('child-component', {
      template: `
        <div>
          <div>计数:{{count}}</div>
          <input ref="myInput" type="text" />
        </div>
      `,
      data: function () {
        return {
          count: 0
        }
      },
      methods: {
        increment() {
          this.count++
        }
      }
    })
    // 父组件
    Vue.component('my-component', {
      template: `
      <div>父组件
        <button @click="callChild">获取父组件</button>
        <child-component v-for="(product, index) in products" 
            :key="index" 
            :item="product" 
            ref="productItems"></child-component>
      </div>`,
      data: function () {
        return {
          products: [
            { name: '商品A' },
            { name: '商品B' },
            { name: '商品C' }
          ]
        }
      },
      methods: {
        callChild() {
          // 访问子组件实例
          console.log(this.$refs);
        }
      }
    })
    // 创建实例
    var app = new Vue({
      el: "#app",
      data: {},
      methods: {},
      computed: {}
    })
  </script>
</body>

$refs 只会在组件渲染完成之后生效,并且它们不是响应式的。这仅作为一个用于直接操作子组件的"逃生舱"------你应该避免在模板或计算属性中访问 $refs

依赖注入

依赖注入(Dependency Injection)是一种高级特性,主要通过 provideinject 这对 API 实现,用于解决组件之间深层传递数据的问题。

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('child-component', {
      template: `
        <div>
          <input ref="myInput" type="text" />
        </div>
      `,
      inject: ['userInfo', 'greet'],
      mounted() {
        console.log('用户信息:', this.userInfo) // { name: '张三', age: 28 }
        this.greet()
      }
    })
    // 父组件
    Vue.component('my-component', {
      template: `
      <div>父组件
        <child-component></child-component>
      </div>`,
      provide: function () {
        return {
          userInfo: { name: '张三', age: 20 },
          greet: () => console.log('Hello from parent')
        }
      },
      methods: {}
    })
    // 创建实例
    var app = new Vue({
      el: "#app",
      data: {},
      methods: {},
      computed: {}
    })
  </script>
</body>

带默认值的注入:

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('child-component', {
      template: `
        <div>
          <input ref="myInput" type="text" />
        </div>
      `,
      inject: {
        userInfo: {
          default: () => ({ name: 'Guest' })
        },
        // 从不同的 key 注入
        userName: {
          from: 'userInfo',
          default: () => ({ name: 'Guest' })
        }
      },
      mounted() {
        console.log('用户信息:', this.userInfo) // { name: '张三', age: 28 }
        console.log('用户信息:', this.userName) // { name: '张三', age: 28 }
      }
    })
    // 父组件
    Vue.component('my-component', {
      template: `
      <div>父组件
        <child-component></child-component>
      </div>`,
      provide: function () {
        return {
          userInfo: { name: '张三', age: 20 }
        }
      },
      methods: {}
    })
    // 创建实例
    var app = new Vue({
      el: "#app",
      data: {},
      methods: {},
      computed: {}
    })
  </script>
</body>

相比 $parent 来说,这个用法可以让我们在任意后代组件中访问,这允许我们更好的持续研发该组件,而不需要担心我们可能会改变/移除一些子组件依赖的东西。同时这些组件之间的接口是始终明确定义的,就和 props 一样。

然而,依赖注入还是有负面影响的。它将你应用程序中的组件与它们当前的组织方式耦合起来,使重构变得更加困难。同时所提供的属性是非响应式的。出于设计的考虑,因为使用它们来创建一个中心化规模化的数据跟使用$root做这件事都是不够好的。如果你想要共享的这个属性是你的应用特有的,而不是通用化的,如果你想在祖先组件中更新所提供的数据,那么这意味着你可能需要换用一个像 Vuex 这样真正的状态管理方案了。

程序化的事件侦听器

程序化的事件侦听器指的是通过代码(而非模板)来动态绑定和移除事件监听,主要使用 $on$off$once 这几个实例方法。

这种方式适合在组件生命周期中根据条件动态管理事件,比如在某些状态下需要监听事件,而在其他状态下需要移除监听(绑定在当前组件实例(this)上的 "虚拟事件",不存在于 DOM 中,只存在于组件的事件系统中)。

  • $on绑定事件监听

为当前组件实例绑定一个或多个事件,当事件被触发时执行对应的处理函数。

js 复制代码
    Vue.component('my-component', {
      template: `
      <div>父组件
        <button @click="active">点击{{count}}</button>
      </div>`,
      data: function () {
        return {
          count: 0
        }
      },
      mounted() {
        // 绑定单个事件
        this.$on('add', (num) => {
          console.log('执行加法:', num + 10)
        })
        // 绑定多个事件
        this.$on({
          'login': () => console.log('登录事件触发'),
          'logout': () => console.log('登出事件触发')
        })
      }
    })
  • $once绑定一次性事件

为当前组件实例绑定一个事件,但该事件只会被触发一次,触发后自动移除监听。

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', {
      template: `
      <div>父组件
        <button @click="handleClick">点击</button>
      </div>`,
      mounted() {
        // 绑定一次性事件
        this.$once('firstClick', () => {
          console.log('这是第一次点击,只会触发一次')
        })
      },
      methods: {
        handleClick() {
          this.$emit('firstClick') // 第一次触发:执行处理函数
          this.$emit('firstClick') // 第二次触发:无反应(已自动移除)
        }
      }
    })
    // 创建实例
    var app = new Vue({
      el: "#app",
      data: {},
      methods: {},
      computed: {}
    })
  </script>
</body>

适用只需要执行一次的操作,避免重复触发(如防止重复提交表单)。

  • $off移除事件监听

移除当前组件实例上绑定的事件监听,防止内存泄漏或不必要的事件触发。

(1)移除指定事件的指定处理函数:this.$off(eventName, handler)

(2)移除指定事件的所有处理函数:this.$off(eventName)

(3)移除所有事件的所有处理函数:this.$off()

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', {
      template: `
      <div>父组件
        <button @click="active">点击{{count}}</button>
        <button @click="unbind">解绑事件</button>
      </div>`,
      data: function () {
        return {
          count: 0
        }
      },
      mounted() {
        // 绑定单个事件
        this.$on('add', (num) => {
          console.log('执行加法:', num)
        })
      },
      methods: {
        active: function(){
          this.$emit('add', this.count++)
        },
        unbind: function(){
          this.$off('add')
        }
      }
    })
    // 创建实例
    var app = new Vue({
      el: "#app",
      data: {},
      methods: {},
      computed: {}
    })
  </script>
</body>

移除事件时,handler 必须与 $on 绑定的是同一个函数(不能是匿名函数,否则无法匹配)。

组件销毁前建议用 this.$off() 清理所有事件,避免内存泄漏。

循环引用

  • 递归组件

当你全局注册一个组件时,这个全局的 ID 会自动设置为该组件的 name 选项:

js 复制代码
Vue.component('my-component', {
  // ...
})

组件是可以在它们自己的模板中调用自身的。局部组件只能通过 name 选项来做这件事,全局组件可以通过IDname选项:

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', {
      name:"stack-overflow",
      template: `
      <div>
       <my-component></my-component>
       <stack-overflow></stack-overflow>
      </div>`
    })
    // 局部组件
    const localComponent = {
      name: 'stack-overflow',
      template: `
      <div>
        <stack-overflow></stack-overflow>
      </div>`
    }
    // 创建实例
    var app = new Vue({
      el: "#app",
      components:{
        localComponent
      }

    })
  </script>
</body>

稍有不慎,递归组件就可能导致无限循环,类似上述的组件将会导致"Maximum call stack size exceeded"错误。

  • 组件之间的循环引用

循环引用指的是两个或多个组件之间相互引用的情况(如 A 组件引入 B 组件,B 组件又引入 A 组件)。这种情况在开发中并不少见(例如树形组件、父子嵌套组件等),但如果处理不当会导致组件加载错误。

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <a-component></a-component>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 全局组件
    Vue.component('a-component', {
      template: `
      <div>
       <b-component></b-component>
      </div>`
    })
    // 局部组件
    const localComponent = {
      template: `
      <div>
        <a-component></a-component>
      </div>`
    }
    // 创建实例
    var app = new Vue({
      el: "#app",
      components:{
        'b-component':localComponent
      }
    })
  </script>
</body>

这段代码会报错(Unknown custom element: <b-component> - did you register the component correctly? For recursive components, make sure to provide the "name" option.)组件未完全注册" 而报错。

html 复制代码
<body>
  <!-- 父组件 -->
  <div id="app">
    <a-component></a-component>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 全局组件 a
    Vue.component('a-component', {
      template: `
      <div>
       <b-component></b-component>
      </div>`
    })
    // 全局组件 b
    Vue.component('b-component', {
      template: `
      <div>
        <a-component></a-component>
      </div>`
    })
    // 创建实例
    var app = new Vue({
      el: "#app"
    })
  </script>
</body>

运行这段代码时不会报 "组件未注册" 的错误,但会导致无限递归渲染,最终触发浏览器的栈溢出错误(Maximum call stack size exceeded)。

官方推荐通过异步导入组件(代码分割)的方式解决循环依赖,但是如果没有添加递归终止条件,依然会导致无限渲染和栈溢出错误。最好的做法就是避免这种写法

模板定义的替代品

  • 内联模板

内联模板(Inline Template)是一种特殊的模板定义方式,允许你在组件的 DOM 元素上直接定义模板内容,而不是在组件内部的 <template> 标签或 template 选项中定义。

html 复制代码
<body>
  <div id="app">
    <h2>父组件</h2>
    <!-- 子组件使用 inline-template -->
    <my-component inline-template child-message="父组件数据">
      <div class="child-content">
        <p>这是子组件的内联模板</p>
        <p>父组件数据: {{ message }}</p> <!-- 无法访问 -->
        <p>子组件数据: {{ childMessage }}</p>
        <button @click="childMethod">调用子组件方法</button>
      </div>
    </my-component>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 全局注册A组件,内部异步导入B组件
    Vue.component('my-component', {
      template: `
        <div>
          {{childMessage}}
        </div>
      `,
      data() {
        return {
          childMessage: '我是子组件的数据'
        }
      },
      methods: {
        childMethod() {
          alert('我是子组件的方法')
        }
      }

    })
    // 创建Vue实例
    var app = new Vue({
      el: "#app",
      data: {
        message: '我是父组件的数据'
      }
    })
  </script>
</body>

内联模板的内容属于子组件的作用域,只能访问子组件的数据和方法,无法直接访问父组件的资源。

最佳实践,请在组件内优先选择 template 选项或 .vue 文件里的一个 <template> 元素来定义模板

  • X-Template

x-template 是另一种定义组件模板的方式,它允许你在 HTML 文档的 <script> 标签中定义模板,通过 ID 与组件关联。这种方式特别适合在非单文件组件(.vue)的场景中使用,比如在传统的 HTML 页面中引入 Vue 时。

html 复制代码
<body>
  <div id="app">
    <h2>父组件</h2>
    <my-component></my-component>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script type="text/x-template" id="my-component-template">
      <div class="child-content">
        <p>这是子组件的内联模板</p>
        <p>子组件数据: {{ childMessage }}</p>
        <button @click="childMethod">调用子组件方法</button>
      </div>
  </script>
  <script>
    // 全局注册A组件,内部异步导入B组件
    Vue.component('my-component', {
      template: '#my-component-template',
      data() {
        return {
          childMessage: '我是子组件的数据'
        }
      },
      methods: {
        childMethod() {
          alert('我是子组件的方法')
        }
      }

    })
    // 创建Vue实例
    var app = new Vue({
      el: "#app",
      data: {
        message: '我是父组件的数据'
      }
    })
  </script>
</body>

模板与组件逻辑分离,不利于代码组织和维护,难以管理。

控制更新

  • 强制更新

组件的更新通常是由响应式数据驱动的,当响应式数据发生变化时,Vue 会自动触发相关组件的重新渲染。但在某些特殊情况下,可能需要手动控制更新或强制更新组件。

如果你发现你自己需要在 Vue 中做一次强制更新,99.9% 的情况,是你在某个地方做错了事。

Vue 实例提供了 $forceUpdate() 方法,可以强制当前组件重新渲染:

html 复制代码
<body>
  <div id="app">
    <h2>父组件</h2>
    <my-component></my-component>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 全局注册A组件,内部异步导入B组件
    Vue.component('my-component', {
      template: `
	      <div> 
	        <h3>当前计数: {{ count }}-{{nonReactiveData.value}}</h3>
	        <button @click="normalUpdate">正常更新</button>
	        <button @click="forceUpdate">强制更新</button>
	        <button @click="directModify">直接修改(不触发更新)</button>
	      </div>
      `,
      data() {
        return {
          count: 0,
          // 非响应式数据示例
          nonReactiveData: { value: 0 }
        }
      },
      methods: {
        normalUpdate() {
          this.count++
        },
        forceUpdate() {
          this.$forceUpdate();
          console.log('已执行强制更新');
        },
        directModify() {
          // 这种方式修改不会触发响应式更新
          this.nonReactiveData.value++;
        }
      }

    })
    // 创建Vue实例
    var app = new Vue({
      el: "#app",
      data: {
        message: '我是父组件的数据'
      }
    })
  </script>
</body>

尽量避免使用 $forceUpdate(),因为它会绕过 Vue 的响应式系统优化(Vue 只会对 data 中的顶层属性添加 getter/setter ,对象里面的属性没有自己的 setter,更新时只是内部值变了,引用没变)。

推荐用 this.$setVue.set 来修改对象内部属性 ------ 它们会手动触发 setter,确保响应式系统能感知到变化。

js 复制代码
//this.$set(目标对象, 属性名, 新值)
this.$set(this.nonReactiveData, 'value', this.nonReactiveData.value + 1);

使用强制更新应该是最后的解决方案,优先考虑正确使用 Vue 的响应式系统来管理数据。

  • 通过 v-once 创建低开销的静态组件

v-once 指令用于标记元素或组件只渲染一次,之后即使数据发生变化也不会重新渲染,这对于创建低开销的静态组件非常有用。

html 复制代码
<body>
  <div id="app">
    <h2>父组件</h2>
    <my-component></my-component>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 全局注册A组件,内部异步导入B组件
    Vue.component('my-component', {
      template: `
      <div v-once> 
        <h3>当前计数: {{ count }}</h3>
        <button @click="normalUpdate">正常更新</button>
      </div>
      `,
      data() {
        return {
          count: 0
        }
      },
      methods: {
        normalUpdate() {
          this.count++
        }
      }

    })
    // 创建Vue实例
    var app = new Vue({
      el: "#app",
      data: {
        message: '我是父组件的数据'
      }
    })
  </script>
</body>

一旦渲染完成,即使父组件或其他地方的数据发生变化,这个组件也不会重新渲染。

不要过度使用这个模式。当你需要渲染大量静态内容时,极少数的情况下它会给你带来便利,除非你非常留意渲染变慢了,不然它完全是没有必要的------再加上它在后期会带来很多困惑。例如,设想另一个开发者并不熟悉 v-once 或漏看了它在模板中,他们可能会花很多个小时去找出模板为什么无法正确更新。

单文本组件

单文本组件(Text Component)是现代前端开发中用于处理文本展示与交互的核心 UI 组件,广泛应用于 ReactVueAngular 等框架及跨端方案中。它通过封装文本相关的通用逻辑,解决了原生 HTML 文本处理中的样式不一致、交互繁琐、跨平台适配等问题。

但当在更复杂的项目中,或者你的前端完全由 JavaScript 驱动的时候,下面这些缺点将变得非常明显:

  • 全局定义 (Global definitions) 强制要求每个 component 中的命名不得重复
  • 字符串模板 (String templates) 缺乏语法高亮,在 HTML 有多行的时候,需要用到丑陋的 \转义符
  • 不支持 CSS (No CSS support) 意味着当 HTMLJavaScript 组件化时,CSS 明显被遗漏
  • 没有构建步骤 (No build step) 限制只能使用 HTMLES5 JavaScript ,而不能使用预处理器,如 Pug (formerly Jade) 和 Babel

文件扩展名为 .vuesingle-file components (单文件组件) 为以上所有问题提供了解决方法,并且还可以使用 webpackBrowserify 等构建工具。

这是一个文件名为 Hello.vue 的简单实例:

html 复制代码
<template>
  <p>{{ message }}</p>
</template>
<script>
  export default {
    data: function () {
      return {
        message: 'hello world'
      }
    }
  }
</script>
<!-- 可选:添加scoped使样式仅作用于当前组件 -->
<style scoped>
  p {
    font-size: 2em;
    text-align: center;
  }
</style>

现在我们获得:

  • 完整语法高亮 :指代码编辑器或 IDE 对不同编程语言的语法结构(如关键字、变量、字符串、注释等)进行差异化颜色标记的功能,且支持多种语言和复杂语法场景。
  • CommonJS 模块 :是一套用于 JavaScript 模块化的规范,主要用于 Node.js 环境,目的是解决 JavaScript 缺乏原生模块化机制的问题。
  • 组件作用域的 CSS :指在前端组件(如 VueReact 组件)中,使 CSS 样式仅作用于当前组件内部元素,避免样式污染全局或其他组件的机制。

如果你和其他开发者一起开发一个大型工程,或有时引入三方 HTML/CSS ,使用 scoped属性作用域会确保你的样式只会运用在它们想要作用的组件上。

.vue 文件是 Vue 框架定义的专用格式,包含 <template><script><style> 三个部分,这种结构需要通过 Vue 的构建工具(如 ViteWebpack )解析处理后,才能转换为浏览器可识别的 HTML /CSS /JS 代码。

在一个组件里,其模板、逻辑和样式是内部耦合的,并且把他们搭配在一起实际上使得组件更加内聚且更可维护。

即便你不喜欢单文件组件,你仍然可以把 JavaScriptCSS 分离成独立的文件然后做到热重载和预编译。

html 复制代码
<!-- my-component.vue -->
<template>
  <div>This will be pre-compiled</div>
</template>
<script src="./my-component.js"></script>
<style src="./my-component.css"></style>

针对刚接触 JavaScript 模块开发系统的用户,如果你没有准备好的话,意味着还需要学会使用一些附加的工具:

  • 安装 Node.js:自带 npm 包管理工具,用于安装依赖(官网下载,LTS 版本即可)。
  • 学会使用 npm 命令:npm install(安装依赖)、npm run dev(启动开发服务器)。
  • 学会用 ViteWebpack 创建 Vue 项目。

了解这些资源之后,你就能很快地运行一个带有 .vue 组件、ES2015webpack 和热重载 (hot-reloading) 的 Vue 项目!

单文件组件应该总是让 <script><template><style> 标签的顺序保持一致。且 <style> 要放在最后,因为另外两个标签至少要有一个。

javascript 复制代码
<!-- ComponentA.vue -->
<template>...</template>
<script>/* ... */</script>
<style>/* ... */</style>

<!-- ComponentB.vue -->
<template>...</template>
<script>/* ... */</script>
<style>/* ... */</style>
相关推荐
赵庆明老师2 小时前
uniapp 微信小程序页面JS模板
javascript·微信小程序·uni-app
程序员勾践2 小时前
前端仅传path路径给后端,避免攻击
前端
登山人在路上2 小时前
Vue 2 中响应式失效的常见情况
开发语言·前端·javascript
董世昌412 小时前
创建对象的方法有哪些?
开发语言·前端
问道飞鱼2 小时前
【前端知识】前端项目不同构建模式的差异
前端·webpack·构建·开发模式·生产模式
be or not to be2 小时前
CSS 布局机制与盒模型详解
前端·css
海市公约2 小时前
JavaScript零基础入门指南:从语法到实战的核心知识点解析
javascript·ecmascript·前端开发·dom·bom·定时器与事件·js语法实战
码界奇点2 小时前
基于Spring Boot和Vue.js的房屋出租管理系统设计与实现
vue.js·spring boot·后端·车载系统·毕业设计·源代码管理
Irene19912 小时前
JavaScript 字符串和数组方法总结(默写版:同9则6 Str21 Arr27)
javascript·字符串·数组·方法总结