组件编写原则

  • data 必须是一个函数(复用时有分开的作用域)
  • 单个根元素
  • 组件名称:
    • ‘my-component-name’:使用时只能是 <my-component-name>
    • ’MyComponentName’:使用时可以 <my-component-name><MyComponentName>

组件注册

全局

可以在 Vue 根实例下的任何地方使用

Vue.component('my-cpn', {
  template: `
    <div>
      <h3>我是全局</h3>
    </div>
  `
})

局部

var myapp = new Vue({
  el: '#myapp',
  components: {
    'my-cpn': {
      template: `
        <div>
          <h3>我是局部</h3>
        </div>
      `
    }
  }
})

组件模板分离写法

方法1. script 标签:

<script type="text/x-template" id="cpn1">
  <div>
    <h3>组件模板分离 script</h3>
  </div>
</script>

方法2. template 标签:

<template id="cpn2">
  <div>
  <h3>组件模板分离 template</h3>
  </div>
</template>

注册:

Vue.component('my-cpn1', {
  template: '#cpn1' // template 对应上面标签的 id
})
Vue.component('my-cpn2', {
  template: '#cpn2' // template 对应上面标签的 id
})

使用 Vue.component 来定义组件有很多缺点:

  • 全局定义强制要求每个 component 中的命名不得重复
  • 字符串模板缺乏语法高亮
  • 不支持 CSS
  • 不能使用预处理器

所以使用 .vue 文件扩展名的单文件组件:

  • CommonJS 模块
  • 完整语法高亮
  • 组件作用域的 CSS
  • 可以使用预处理器

Vue.js 定义组件模板的 7 种方式 - 知乎

父子组件通信

父传子:props

// 在标签中动态绑定 v-bind:子组件中props定义的变量="父组件中对应的变量"
<div id="myapp">
  <cpn v-bind:parentmsg="message"></cpn>
</div>
 
// 在子组件模板中使用props定义的变量
<template id="cpn">
  <p>我是子组件,接收到{{ parentmsg }}</p>
</template>
 
<script>
  const cpn = {
    template: '#cpn',
 
    props: ['parentmsg'] // 父组件传递过来的数据,子组件只读
    
    data() {
      return {
        mydata: '子组件的值' // 子组件私有的数据,子组件可读可写
      }
    },
  }
 
  var myapp = new Vue({
    el: '#myapp',
    data: {
      message: '父组件的值'
    },
    // 子组件注册
    components: {
      cpn // 对象属性简写,左右两边相同时的简写,等同写法 cpn: cpn
    }
  })
</script>

特别地:v-bind=“obj” 等同于把 obj 对象里的所有属性传进去

子组件中属性 props 的类型:

  1. 字符串数组形式:props: ['para1', 'para2', 'para3']
  2. 对象形式:(一般用对象)
props: {
  // 指定类型,有 String, Number, Boolean, Array, Object, Function, Date, Symbol
  para1: String,
  para2: [String, Number], // 多个可能的类型
  para3: {
    type: String,
    required: true // required表示必须传值
  }
  para4: {
    type: Array,
    // 类型是对象或数组 Object/Array 时,默认值必须是一个函数
    default: function() {
      return []
    }
  }
}

注意:

  • prop 命名在 JS 中是小驼峰(camelCase),HTML 中是短横线(kebab-case)
  • 父子组件之间是单向下行绑定,子组件不要改变父组件的 props
  • 每次父级组件发生变更时,子组件中所有的 prop 都将会刷新为最新的值
  • 如果需要子组件中的值同步 props 的值更新,用计算属性
  • 如果传入一个参数,但是子组件中没有定义,那么会自动绑定到子组件的根元素上。可以在子组件中使用 inheritAttrs: false 来防止自动绑定(除了 class 和 style)
  • this.$attrs 表示组件中没有定义的传入的 props

子传父:$emit

<div id="myapp">
  {{ message }}
  <!-- 自定义事件my-event,触发时调用父组件的cpnclick方法 -->
  <cpn v-on:my-event="cpnclick"></cpn>
</div>
 
<template id="cpn">
  <!-- 子组件监听点击事件触发myclick -->
  <p v-on:click="myclick('abc')">我是子组件,点我传值 abc 给父组件</p>
</template>
 
const cpn = {
  template: '#cpn',
  methods: {
    // 监听到事件发生时,发送自定义事件给父组件,value 是'abc'
    myclick(value) {
      this.$emit('my-event', value)
    }
  }
}
 
var myapp = new Vue({
  el: '#myapp',
  data: {
    message: '父组件的值'
  },
  // 子组件
  components: {
    cpn // 对象属性简写,左右两边相同时的简写,等同写法 cpn: cpn
  },
  methods: {
    cpnclick(value) {
      this.message = '接收到子组件的值:' + value
    }
  }
})

注意:

  • 自定义事件不存在任何自动化的大小写转换,始终使用短横线(kebab-case)的事件名
  • this.$listeners 表示组件上绑定的所有事件
  • 父子组件的 prop 如果需要“双向绑定”进行同步更新,正确的做法是在子组件中 emit 一个自定义事件,同时有一个 .sync 语法糖:
<my-cpn
  v-bind:title="title"
  v-on:update:title="title = $event"
></my-cpn>
 
this.$emit('update:title', newTitle) // 子组件中修改值
 
 
// 此情况下,等同于:
<my-cpn v-bind:title.sync="title"></my-cpn>
 
this.$emit('update:title', newTitle)

父组件监听子组件自定义事件时,在接受子组件参数的同时传入自定义参数:

// 子组件:
this.$emit('custom', value1, value2, ...)
 
// 父组件:
<child @custom="parentMethod(...arguments, newValue)"></child>
// arguments 就是子组件传出来的值

https://github.com/vuejs/vue/issues/5735

父访问子:$refs 或者 $children

一般用 $refs,要访问所有子组件时会用 $children

<!-- 父组件 -->
<div id="myapp">
  <cpn></cpn>
  <!-- 给这个子组件命名为aaa -->
  <cpn ref="aaa"></cpn>
  <button @click="btnClick">按钮</button>
</div>
 
<!-- 子组件 -->
<template id="cpn">
  <div>我是子组件</div>
</template>
 
<script>
  // 父组件
  const myapp = new Vue({
    el: '#myapp',
    data: {
      message: '父组件的值'
    },
    methods: {
      btnClick() {
        // 1.第一种方法:$children
        // 打印出子组件
        console.log(this.$children);
        // 调用[第一个]子组件的showMessage方法
        this.$children[0].showMessage();
 
        // 2.第二种方法:$refs
        // 打印出名字为aaa的子组件里的值
        console.log(this.$refs.aaa.message);
      }
    },
    // 子组件
    components: {
      cpn: {
        template: '#cpn',
        data() {
          return {
            message: '子组件的值'
          }
        },
        methods: {
          showMessage() {
            console.log('子组件的showMessage()被调用');
          }
        }
      }
    },
  })
</script>

$refs 只会在组件渲染完成之后生效,并且它们不是响应式的
$children 并不保证顺序,也不是响应式的

子访问父和根:$parent 或者 $root

一般不用,因为会导致父子组件耦合度变大,出了问题很难调试发现是哪里变更了数据

  • $parent:父实例
  • $root:根实例

父组件监听子组件生命周期事件

在子组件上监听 hook 事件:

// 子组件
<child @hook:mounted="getDom"></child>
 
// 父组件
methods:{
   getDom() {
      // 获取子元素等操作
   }
} 

Vue官方文档里没告诉你的神秘钩子——@hook一个关于“如何实现父组件监听子组件生命周期“引发的vue开发小技巧;揭秘 - 掘金

父子组件传递引用值

vue组件引用传值的最佳实践 - 知乎

跨层级祖孙通信

1. provideinject

provide 和 inject 绑定并不是可响应的,如果要想 inject 接受的变量是响应式的,provide 提供的变量本身就需要是响应式的

// 父级组件提供 'foo'
var Provider = {
  provide: {
    foo: 'bar'
  },
  // ...
}
 
// 子组件注入 'foo'
var Child = {
  inject: ['foo'],
  created () {
    console.log(this.foo) // => "bar"
  }
  // ...
}

2. $attrs$listeners

仅仅用来中间传递数据而不做处理,即数据透传

实现:中间组件在其子组件上加上 v-bind="$attrs"v-on="$listeners",将它的父组件的数据传向它的子组件
其中:$attrs 是没有在 props 中定义的,$listeners 是绑定的所有事件

其他组件通信方式

vuex

全局状态管理,适用于多个组件共同维护数据,且对数据有一定的处理

EventBus

原理是 发布订阅模式 结合 $emit $on $off

1.定义一个事件中心:实质上 EventBus 是一个不具备 DOM 的 vue 实例

// event-bus.js
import Vue from 'vue'
export const EventBus = new Vue()

2.组件 A 发送一个事件

<script> 
import { EventBus } from "./event-bus.js"
 
export default {
  methods: {
    sendMsg() {
      EventBus.$emit("msg", '来自A页面的消息');
    }
  }
}; 
</script>

3.组件 B 中接受 A 的事件

<script> 
import { EventBus } from "./event-bus.js"
 
export default {
  mounted() {
    EventBus.$on('msg', msg => {
      // ...
    })
  }
}; 
</script>

4.移除事件监听

EventBus.$off('msg')

localStorage / sessionStorage

注意用 JSON.parse() / JSON.stringify() 做数据格式转换
可以和 vuex 结合进行数据持久化保存

组件通信总结

方式优点缺点
prop$emit常用只能父子
$refs$children偶尔用父子组件耦合
$parent$root基本不用父子组件严重耦合
provideinject跨层级传数据方便不清楚数据来源与变更
$attrs$listeners数据透传中间组件多余代码
vuex数据集中管理,可追溯代码比较复杂
EventBus各种不同组件间通信很方便,代码简洁事件多了后,难以对事件进行维护
localStorage
sessionStorage
简单,浏览器自带数据和状态比较混乱,不容易维护

https://juejin.cn/post/7038457201052090376

动态组件

在 component 内置组件上使用 is 来切换不同的组件:<component v-bind:is="xxx"></component>

如果在切换的时候想保存之前组件的状态:使用 keep-alive

<keep-alive>
  <component v-bind:is="xxx"></component>
</keep-alive>

异步组件

当需要用到组件时才去加载

使用 import 动态加载:() => import('./my-async-component')