Skip to content

为了保证组件的复用性,组件中的 data 必须是一个函数。

组件内部定义需要是单个根元素

9-1.全局注册

全局注册,适用于注册全局组件

注册一次,全局可用。

js
Vue.component('ComponentA', {
  template: '',
  data () {
    return {}
  },
  methods: {}
})

TIP

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

自动化全局注册

在现实的工程化开发中,人工手动全局注册一个个的组件,显得非常麻烦,代码也会非常冗余。

假设有以下目录:

.
├── components
│   ├── BaseButton.vue
│   ├── index.js
│   └── table
│       └── BaseTable.vue

webpack 中的 require.context 为例:

js
import upperFirst from 'lodash/upperFirst'
import camelCase from 'lodash/camelCase'

// 获取该文件的执行上下文 返回值是一个函数
const context = require.context('../components', true, /\.vue$/)

// 可利用属性keys获取该上下文的符合条件的文件名数组集合
context.keys().forEach(filePath => {
  // 根据文件路径获取大写组件名 譬如 ./table/BaseTable.vue 的结果是 BaseTable
  const componentName = upperFirst(camelCase(filePath.spilit('/'.pop().replace(/\.\w+$/, ''))))
  // 根据文件路径获取组件相关信息
  const componentInfo = context(filePath)
  // 全局注册 
  Vue.component(
    componentName,
    // 如果这个组件选项是通过 `export default` 导出的,
    // 那么就会优先使用 `.default`,
    // 否则回退到使用模块的根。
    componentInfo.default || componentInfo
  )
})

9-2.局部注册

局部注册相对于全局注册的一个优势,在使用形如 webpack 的构建系统时,不会打包冗余代码,有利于 tree-shaking

js
import ComponentA from 'path/to/ComponentA'

const ComponentB = {
  components: {
    ComponentA
  },
  data () {
    return {}
  },
  methods: {}
}

9-3.prop

  1. 单向数据流

如果改变了 prop,会报错:

Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value.

TIP

注意,这里的改变 prop 指的是改变了 prop 的指向

如果 prop 是对象 object,那么改变其属性时不会触发报错(并没有改变其地址指向),直接改变其指向才会报错。

但在子组件中改变对象类型的 prop 属性将会影响到父组件的状态。

因为基本数据类型的映射是,复杂数据类型的映射是引用

  1. 非 Prop 的 Attribute

propattribute 默认会作用于子组件的根元素上,并且会覆盖该根元素上设置的属性(classstyle 较为特殊,这俩会重新组合属性)。

  1. 禁用 Attribute 继承

设置 inheritAttrs: false,子组件的根元素将不会继承父组件内设置的 attribute

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

可利用 inheritAttrs: false 结合 v-bind="$attrs",指定子组件内的目标元素接收父组件内设置的 attribute。(同样,这里 styleclass 的绑定也不会受影响

9-4.自定义事件

  1. 自定义 v-model

v-model 默认等于 v-bind:valuev-on:input="value = $event.target.value"

所以 v-model 会改变 value 属性的值。

在单选框 radio 或者复选框 checkbox 上,v-model 如果绑定的是基本类型的值,则会将 value 修改为 truefalse

在某些表单下(譬如 checkbox), value 属性是可以结合 name 属性通过 form 表单提交到服务端的,但 value 不能是 truefalse,即使在 DOM 中布尔值被认作字符串。

通过上述论述,可知在某些场景下,表单元素需要 value 属性进行其他操作时,此时我们不能粗暴的直接使用 v-model 进行绑定,Vue 对此提供了 modle 属性供我们自定义 v-model 绑定。

js
Vue.component('base-checkbox', {
  model: {
    prop: 'checked',
    event: 'change'
  },
  props: {
    checked: Boolean
  },
  template: `
    <input
      type="checkbox"
      v-bind:checked="checked"
      v-on:change="$emit('change', $event.target.checked)"
    >
  `
})
  1. $listeners
js
Vue.component('base-input', {
  inheritAttrs: false,
  props: ['label', 'value'],
  computed: {
    inputListeners: function () {
      var vm = this
      // `Object.assign` 将所有的对象合并为一个新对象
      return Object.assign({},
        // 我们从父级添加所有的监听器
        this.$listeners,
        // 然后我们添加自定义监听器,
        // 或覆写一些监听器的行为
        {
          // 这里确保组件配合 `v-model` 的工作
          input: function (event) {
            vm.$emit('input', event.target.value)
          }
        }
      )
    }
  },
  template: `
    <label>
      {{ label }}
      <input
        v-bind="$attrs"
        v-bind:value="value"
        v-on="inputListeners"
      >
    </label>
  `
})

9-5.插槽

插槽

在子组件中

使用 <slot></slot> 预留默认插槽。

使用 <slot name="name"></slot> 预留具名插槽。

使用 <slot name="name" :prop="prop"></slot> 来预留具名作用域插槽。

在父组件中

  1. 新语法

2.6.0 起的新版本语法,提供了 v-slot,可缩写为 #

推荐在所有情况下都用在 <template> 标签上。

默认插槽,v-slotv-slot:default

具名插槽,v-slot:[name]

作用域插槽,v-slot:[name]="slotProps"

  1. 旧语法

旧语法,是 slotslot-scope

默认插槽,直接填充 或 slot="default"

具名插槽,slot="name"

作用域插槽,slot="name" slot-scope="slotProps"

  1. 编译作用域

TIP

父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。

9-6.动态组件&异步组件

  1. 动态组件

动态组件可利用 is 属性进行切换。

组件会重新渲染。

① 利用 元素的 is 属性

is 属性应该用 v-bind 修饰(可以简写为 : )

is 属性应该传入注册的组件名

如果想要动态组件保留状态,可以使用 <keep-alive> 来缓存。

TIP

条件渲染 v-if 与动态组件 :is 的区别:

  1. 条件渲染

    • 工作方式:根据 v-ifv-else-if 的条件,Vue 会决定渲染 TextComponentImageComponent、或 JsonComponent
    • 优缺点:每个组件的渲染条件都是独立的。如果有多个条件判断(比如更多的组件类型),代码会变得较冗长。
    • 性能:条件渲染会根据条件判断只创建一个满足条件的组件,但每次重新渲染时,Vue 可能会销毁和重建组件。
  2. 动态组件

    • 工作方式:通过 :is 绑定动态指定的组件名称,Vue 自动渲染与 item 变量相对应的组件,比如 TextComponentImageComponentJsonComponent
    • 优缺点:代码更加简洁,尤其是当有很多种可能的组件类型时,无需为每种类型写条件判断。
    • 性能:动态组件会复用现有组件实例,减少频繁的销毁和重建。动态组件的显示和隐藏通常不会销毁组件实例(除非设置 key 强制重新渲染)。
  1. 异步组件

异步组件根据 resolvereject 的执行情况,决定组件的渲染。

支持以下两种用法:

js
components: {
	'my-component': () => import('./my-async-component')
}

// vue@2.3.0+,vue-router@2.4.0+
components: {
	'my-component': () => ({
		// 需要加载的组件 (应该是一个 `Promise` 对象)
		component: import('./MyComponent.vue'),
		// 异步组件加载时使用的组件
		loading: LoadingComponent,
		// 加载失败时使用的组件
		error: ErrorComponent,
		// 展示加载时组件的延时时间。默认值是 200 (毫秒)
		delay: 200,
		// 如果提供了超时时间且组件加载也超时了,
		// 则使用加载失败时使用的组件。默认值是:`Infinity`
		timeout: 3000
	})
}

Webpack 提供了code-splitting来将 import() 语法的相关 chunks 自动抽离为 Async chunks

另外,Webpack 还会提供 PrefetchPreload 来优化异步组件的加载。

异步组件整体执行逻辑如下:

  1. 在打包时,Webpack 会根据 import() 语法,将相关 chunks 抽离为 Async chunks

  2. Vue 的异步路由被触发时,webpack_require 会被调用,利用 JSONP 来获取 Async chunks

  3. Async chunks 加载成功后,会调用 resolve 方法,Vue 会根据组件名称进行渲染。

  4. 如果加载失败,会调用 reject 方法,Vue 会根据组件名称进行渲染。