一万字总结Vue2.6.x

8,523 阅读9分钟

Vue

官网介绍它是一个构建用户界面的渐进式框架 ;

渐进式框架 : 主张最少 , 每个框架都不可避免会有自己的一些特点 , 从而对使用者有一定的要求 , 这些要求就是主张 , 主张有强有弱,它的强势程度会影响在业务开发中的使用方式 ; 而 Vue 虽然有全家桶套餐 , 但是你可以只用它的一部分 , 而不是用了它的 核心库 就必须用它的全部 .

声明式渲染

Vue.js 提供了简洁的模板语法声明式的将数据渲染至 DOM 中

<div id="app">
	{{ message }}
</div>
const vm = new Vue({
	el: '#app',
	data: {
		message: 'hello vue'
	}
})
  • el :  元素挂载点;只有在 new 创建实例的时候生效 ; 实例挂载之后可以使用 vm.$el 访问
  • data : Vue 实例的数据对象 , Vue 会递归的将 data 的 property 转换为 getter 和 setter , 从而让 data 的 property 能够响应数据变化 ; 对象必须是纯粹的对象 (含有 0 个或者 多个 键值对) 浏览器 API 创建的对象 , 原型上的 property 会被忽略 , 大概来说 data 只能存在数据 , 不推荐观察拥有状态行为的对象 ;
  • {{}} : 插值表达式 ; 官网也称为 Mustache 语法

为什么组件中 data 是方法

当一个组件被定义时 (非根组件) data 必须声明为一个返回对象的函数 , 因为组件可能被用来创建多个实例, 如果 data 仍然是一个对象 , 这样所有实例讲共享引用同一个数据对象 , 通过提供 data 函数 , 每次创建一个实例的时候 , 我们能够调用 data 函数 , 从而返回初始数据的一个全新数据对象

// 错误 示例
let options = {
  data: {
    uname: 'zs'
  }
}
function Component(options) {
  this.data = options.data
}

let user1 = new Component(options)
let user2 = new Component(options)

user1.data.uname = 'ls' // 修改 user1  触发了所有
console.log(user2.data.uname) // ls
// 正确示例
let options = {
  data() {
    return {
      uname: 'zs'
    }
  }
}
function Component(options) {
  this.data = options.data()
}

let user1 = new Component(options)
let user2 = new Component(options)

user1.data.uname = 'ls' 
console.log(user2.data.uname) // zs
console.log(user1.data.uname) // ls

由于组件是可以多次复用的 , 如果不使用 function return 每个组件的 data 在内存中都是指向同一个地址的 , 由于 JavaScript 复杂数据类型的特性 , 那一个数据改变其他的也改变了 , 但是如果用了 function return 其实就相当于申明了新变量 , 相互独立 , 自然就不存在以上例子中存在的问题 ; JavaScript 在赋值 Object 时 , 是直接一个相同的内存地址 , 所以为了每个组件的独立 , 采用了这种方式 ; 但由于根组件只有一个 , 不存在数据污染的情况 , 所以就可以是一个对象 ;

参考资料 :

指令

v-cloak

这个指令可以配合着 CSS 隐藏未编译的 Mustache 标签 , 直到实例准备完毕

问题展示 :

/* css */
[v-cloak] { display: none; }
<div v-cloak>{{ root }}</div>

v-text

更新某个元素节点下的值 ; 注意 : 会更新全部内容 , 如果想要局部更新 , 可以使用 Mustache 语法

<div v-text="root"></div>

v-html

更新元素的 innerHTML 注意 : 普通 html 内容

在网站上使用 HTML 是非常危险的 , 容易导致 XSS 工具 , 用户提交内容时 切记勿要使用

<div v-html="html"></div>
new Vue({
  el: '#app',
  data: {
    html: '<p>hello vue</p>'
  }
})

v-pre

原文输出 , 不会参与编译 , 输入什么内容就展示什么内容

<div v-pre>{{ will not compile }}</div>

v-once

被定义了 v-once 指令的元素或者组件 (包括元素或组件内的子孙节点) 只能被渲染一次 , 首次渲染收 , 即时数据发生变化 , 也不会被重新渲染 , 一般用于静态内容展示 ;

<div v-once>{{ content }}</div>
const vm = new Vue({
  el: '#app',
  data: {
    content: 'this is init data'
  }
})
vm.content = 'update data'

v-showv-if

这里的 v-if 不单单是这一个指令 , 它包含 v-else-if v-else 功能差不多 , 这里就统一解释了

v-show : 根据表达式的真假值 , 判断元素是否隐藏 ( 切换元素的 display : block/none )

v-if : 根据表达式的值来有条件的渲染数据 , 在切换时元素以及它的数据绑定 / 组件被销毁并重建

差异 :

<div v-if="isShow"> v-if </div>
<div v-show="isShow"> v-show </div>

分支判断代码演示

<!-- 最终一会展示一个 p 标签中的内容  -->
<div>
  <p v-if="score > 90"> 
  	<span>成绩优异 : {{ score }}</span>
  </p>
  <p v-else-if="score > 70"> 
  	<span>成绩及格 : {{ score }}</span>
  </p>
  <p v-else> 
  	<span>不及格 : {{ score }}</span>
  </p>
</div>

v-for

v-for 中被循环的对象 , 必须是一个可迭代对象 iterable ( Array | Number | Object | String ... )

语法格式为 alias in expression 其中的 in 也可以使用 of 替代

可以为数组或者对象增加索引值

<!-- 数组循环 -->
<div v-for="(item, index) in items">
  {{ item.text }}
</div>
<!-- 对象循环 -->
<div v-for="(val, key, index) in object">
  {{ val }} {{ key }} {{ index }}
</div>

为什么 v-for 必须添加唯一 key

当 Vue 正在更新使用 v-for 渲染的数据列表时 , 它默认使用 就地更新 策略 , 如果数据项的顺序被改变 , Vue 将不会移动 DOM 来匹配数据项的数据 , 而是就地更新每个元素 , 保证它们在每个索引位置的正确渲染 ;

为什么要加 key

  • 为了给 Vue 一个提示 , 以便它跟踪每个节点的身份 , 从而重用 和 重新排序现有元素 需要为每一项提供一个唯一 key
  • key 主要用在 Vue 的 虚拟 DOM 算法 , 在新旧节点对比时 , 辨识虚拟DOM , 如果不使用 key 会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法 , 而如果使用了key 它会基于 key 的变化重新排列元素顺序 , 并且会移除 key 不存在的元素

为什么不能用 indexkey

// 组件数据定义
const vm = new Vue({
  el: '#app',
  data: {
    users: [
      { id: 1, uname: 'zs', age: 23 },
	     { id: 2, uname: 'ls', age: 24 },
	     { id: 3, uname: 'we', age: 25 },
	     { id: 4, uname: 'mz', age: 26 },
	  ]
  }
})

index 错误示例

重点在于上面我们所说的会基于 Key 的变化重新排列元素顺序 , 可以看出如果我们用 index 作为 key 数组翻转的时候 , 其实 Key 的顺序是没有变的 , 但是传入的值完全变了 , 这时候原本不一样的数据 , 被误以为一样了 , 所以就造成以下问题 ;

<!-- 具体语法稍后介绍; 意思为点击翻转数组 -->
<button @click="users.reverse()">年龄排序</button>
<ul>
  <!-- 循环这个 users 生成一个数据列表 并且里面带有 多选框 以供我们测试 -->
  <li v-for="(user, index) of users" :key="index">
    <input type="checkbox" />
    <span>{{ user.uname }}</span>
  </li>
</ul>

唯一 Id 正确示例

此时的 key 和数据做绑定 , 当你翻转数组的时候 , 绑定的其实是这一条数据 , 而不是索引 , 就不会造成以上问题了

<li v-for="(user, index) of users" :key="user.id">
  ......
</li>

参考资料 :

https://juejin.cn/post/6844904113587634184

v-bind

属性绑定 ; 可缩写为 :

// 绑定 attrbute
<div v-bind:content="message"></div>

// 绑定 class 
<div :class="{box: isBox}"></div>
<div :class="['box', 'box1']"></div>
<div :class="['box', {box1: isBox}]"></div>

// 绑定 style
<div :style="{fontSize: '20px', color: 'white'}"></div>
<div :style="[{fontSize: '20px'}, {color: 'white'}]"></div>

v-on

事件绑定 ; 可缩写 @ 监听DOM事件 , 并在触发时运行一些 js 代码

<button v-on:click="count += 1"></button>
{{ count }}

可以接收一个方法名称 ;

注意 : 当只是一个方法名称时, 默认第一个参数为事件对象 e

当需要传入参数时 , 那么事件对象就需要手动的传入, 最后一个 并且强制写成 $event

<button @click="handle">点击1</button>
<button @click="handle1('content', $event)">点击2</button>
methods: {
  handle(e) {
    console.log(e.target)
  },
  handle1(ct, e) {
  	console.log(ct)
    console.log(e.target)
  }
}

事件修饰符

  • .prevent : 阻止默认事件
  • .stop : 阻止冒泡
  • .self : 只有当前元素触发事件
  • .once : 只触发一次该事件
  • .native : 监听组件根元素的原生事件
// 定义子组件
Vue.component('my-component', {
  template: `
    <button @mousedown="handle" :style="{color: 'white', lineHeight: '20px', backgroundColor: 'black'}">组件</button>
  `,
  methods: {
    handle() {
      console.log('///')
    }
  }
})

加了 native 相当于把自定义组件看成了 html 可以直接在上面监听原生事件, 否则自定义组件上面绑定的就是自定义事件 , 而你在自定义事件上没有定义这个事件 , 所以不加 native 不会执行

// 父组件中引用
<my-component @click.native="handle('父组件')"></my-component>

参考资料 :

https://segmentfault.com/q/1010000011186651

  • .capture : 添加事件监听时 , 使用 捕获模式
// 此时会优先捕获 box1
<div class="box" style="background: skyblue;  width: 180px;" @click.capture="handle('box1')">
  <div class="box1 box" style="background: slateblue; width: 140px;" @click="handle('box2')">
    <div class="box2 box" style="background: red;" @click="handle('box3')"></div>
  </div>
</div>
// 允许只有修饰符 prevent 阻止默认事件
<a href="http://www.baidu.com" @click.prevent >百度</a>

// 多个事件修饰符可以连用触发时机也是相同的
<a href="http://www.baidu.com" @click.prevent.stop="handle('a')">baidu</a>

按键修饰符

Vue 中允许为 v-on 监听键盘事件时添加键盘修饰符

<input v-on:keyup.enter="submit">

当然提供了大多数的按键码别名 按键码

还可以通过全局 Vue.config.keyCodes 自定义修饰符别名

Vue.config.keyCodes.f1 = 112

v-model

在表单元素上创建数据双向绑定 , 它会根据控件类型自动选取正确的值来更新元素

// 文本
<input type="text" v-model="message">
<p>{{ message }}</p>
// 多行文本
<textarea cols="30" rows="10" v-model="message"></textarea>
<p>{{ message }}</p>
// 单选框
<input type="radio" value="男" v-model="sex">男
<input type="radio" value="女" v-model="sex">女
<p>{{ sex }}</p>
// 单个复选框
<input type="checkbox" v-model="checked">
<p>{{ checked }}</p>
// 多个复选框
<input type="checkbox" value="打篮球" v-model="hobby"/>打篮球
<input type="checkbox" value="打皮球" v-model="hobby"/>打皮球
<input type="checkbox" value="打气球" v-model="hobby"/>打气球
<input type="checkbox" value="打棒球" v-model="hobby"/>打棒球
<p>{{ hobby }}</p>
// 选择框 -> 单选
<select v-model="selected">
  <option>javascript</option>
  <option>html</option>
  <option>css</option>
</select>
<p>{{ selected }}</p>
// 选择框 -> 多选
<select v-model="selectList" multiple>
  <option>javascript</option>
  <option>html</option>
  <option>css</option>
</select>
<p>{{ selectList }}</p>
// 实例对象
new Vue({
  el: '#app',
  data: {
    message: '', // 多行, 单行文本
    sex: '', // 单选框
    checked: false, // 复选框单个
    hobby: [], // 复选框多个
    selected: '', // 选择框 -> 单个
    selectList: [] // 选择框 -> 多个
  }
})

修饰符

  • .lazy : 默认情况下 v-model 在每次的 input 事件触发后将输入框内容进行同步 , 添加 lazy 修饰符后 , 会变成 change 事件后同步数据
<input v-model.lazy="message">
  • .number : 用户输入的值转为数值类型
<input v-model.number="age">
  • .trim : 过滤输入框中的左右空白
<input v-model.trim="message">

Vue.set

如果在实例创建之后添加新的属性到实例上 , 它不会触发更新视图 怎么理解呢 ?

data() {
  return {
    info: {
      uname: 'zs'
    }
  }
}
mounted() {
  // 此时是不会生效的 , 如果再模块化的开发中还会报错
  this.info.age = 23
}

受 ES5 的限制 , Vue 不能检测到对象属性的添加或者删除 , 因为 Vue 在初始化的时候将属性转换为getter setter 所以属性必须要在 data 对象上才能让 Vue 转换 , 只有在 data 对象上 才是响应式的

mounted() {
  // 正确写法
  this.$set(this.info, 'age', 23)
}

Vue.set() : 与 this.$set 没有区别, 一个全局 一个局部 官网说 this.$setVue.set 的一个别名

methods

methods 将会被混入到Vue 实例中 , 可以直接通过 vm 实例访问这些方法 , 或者在指令表达式中使用 , 方法中的 this 自动绑定 Vue 实例

注意 : methods 中的 方法 不要使用 箭头函数 , 箭头函数中的 this 指向父级作用域的上下文 , 所以 this 将不会指向 Vue 实例

new Vue({
  methods: {
    handle() {
      console.log(this)
    }
  }
})

计算属性 computed

模板内写表达式固然是很方便的 , 但是你应该明白 , 表达式的初衷是用来计算的 , 比如处理一些字符串 , 时间格式等等 , 如果我们写成方法吧 ! 每次都要去调用 , 那就太麻烦了 , 为此 Vue 提供了 计算属性 computed

可以看出每次我们都去调用这个参数 , 感觉很不方便

<input type="text" v-model.number="input1"/> + 
<input type="text" v-model.number="input2"/> =
<span>{{ getSum() }}</span>
data() {
  return {
    sum: '',
    input1: '',
    input2: ''
  }
},
methods: {
  getSum() {
    return this.sum = this.input1 + this.input2
  }
}

下面我们使用计算属性解决 ; 可以看出我们去除了 data 中的 sum 属性 在 computed 中新增了 getSum 函数

<input type="text" v-model.number="input1"/> + 
<input type="text" v-model.number="input2"/> =
<span>{{ getSum }}</span>
data() {
  return {
    input1: '',
    input2: ''
  }
},
computed: {
  getSum() {
    return this.input1 + this.input2
  }
},

那么问题来了 为什么定义了一个函数, 却当成属性执行 ? 其实这个只是简写而已 , 算是一个语法糖 , 每一个计算属性包含 get 和 set 当只有 get 时可以简写为 函数的格式

export default {
  computed: {
    getSum: {
      get() {
       // 获取数据
      },
      set(val) {
        // val 是这个计算属性被修改之后的数据  设置数据
      }
    }
  }
}

示例 : 看完这个例子就明白了为什么叫 计算属性了吧

<input type="text" v-model.number="input1"/> + 
<input type="text" v-model.number="input2"/> =
<span>{{ getSum }}</span>
<!-- set 函数可以接收这里传递过来的值 -->
<button @click="getSum = '未知数'">修改 getSum</button>
computed: {
  getSum: {
    get() {
      return this.input1  + this.input2
    },
    // 接收 getSum 这个属性改变后的值
    set(val) {
      console.log(val)
      this.input1 = 20
      this.input2 = 30
    }
  }
},

侦听属性 watch

虽然计算属性在大多数情况下都适用 , 但有时也需要一个自定义的侦听器 , 这个时候就需要 侦听属性 watch

仍然是计算两数之和 ; 在 watch 监听了 input1 的属性 input1 触发时 求出 sum ; 仔细看已经出现了问题, 修改 input2 的时候就不会再触发了 ;

总结 : 它监听 data 某一个属性的变化 , 并不会创造新的属性

<input type="text" v-model.number="input1"/> + 
<input type="text" v-model.number="input2"/> =
<span>{{ getSum }}</span>
data() {
  return {
    input1: '',
    input2: '',
    getSum: ''
  }
},
watch: {
  // 这样写看着是一个函数, 和属性理解不一致, 当然还可以写成这样
  input1(newVal, oldVal) {
    console.log(newVal, oldVal)
    this.getSum = this.input1 + this.input2
  }
  input2: {
    // 回调函数监听 input2 的变化  函数名必须是 handler
    handler(newVal, oldVal) {
      console.log(newVal, oldVal)
      this.getSum = this.input1 + this.input2
    }
  }
}

如果我们需要侦听对象属性, 可以在选项参数中使用 deep: true 注意监听数据的变更不需要这么做

watch: {
  obj: {
    handler() {
      // ....
    },
    deep: true 
  }
}

watch 使用时有一个特点 , 就是当值第一次绑定的时候 , 不会执行监听函数 , 只有值发生改变时才会执行 , 如果我们需要在最初绑定值的时候也执行函数 , 则需要用到 immediate: true

watch: {
  apiData: {
    handler(newVal, oldVal) { },
    deep: true,
    immediate: true
  }
}

methods computed watch 区别

  • watch 就是单纯的监听某个数据的变化 , 支持深度监听 , 接收两个参数一个最新值, 一个变化前的旧值 , 结果不会被缓存 , 并且 watch 可以处理异步任务

  • computed 是计算属性, 依赖于某个或者某些属性值 , 计算出来的结果会出现缓存 , 只有当数据的依赖项变化时才会发生变化 , 会创建一个新的属性

  • methods 是函数调用 , 没有缓存 , 主要处理一些业务逻辑, 而不是监听或者计算一些属性

过滤器 filter

可以被用于一些常见的文本格式化 , 允许被应用在两个地方 {{}} v-bind

{{ msg | formatMsg }}
<div v-bind:msg="msg | formatMsg"></div>
  • 可以在组件的选项中定义组件内私有的过滤器
Vue.component('son-component', {
  template: `
    <div>{{ msg | formatMsg }}</div>
  `,
  data() {
    return {
      msg: 'this is message'
    }
  },
  filters: {
    formatMsg(msg) {
      return msg.toString().toUpperCase()
    }
  }
})
  • 可以在创建 Vue 实例之前定义全局过滤器
Vue.filter('formatMsg', function(msg) {
  return msg.toString().toUpperCase()
})
  • 过滤器默认是以 | 前面的的内容作为过滤器的第一个参数 , 还可以再次传入传输
<div>{{ msg | formatMsg('lower') }}</div>
Vue.filter('formatMsg', function(msg, args) {
  console.log(msg) // lower
  if (args === 'lower') {
    return msg.toString().toLowerCase()
  }
})

自定义指令 directive

与上面提到的指令一致 , 如果那些指令不能满足使用要求 , 可以自己进行定制

自定获取焦点案例

<input type="text" v-focus/>
// 全局指令 定义时不需要 v-  调用时要加上 v- 前缀
Vue.directive('focus', {
  inserted(el) {
    el.focus()
  }
})
// 或者可以定义为局部
directives: {
  'focus': {
    inserted(el) {
      el.focus()
    }
  }
}

钩子函数

  • bind : 只调用一次 , 指令第一次绑定元素时调用 , 在这里可以进行一次性的初始化设置 ;
  • inserted : 被绑定元素插入父节点时调用 , 不一定渲染完成 , html 已经创建好了
  • update : 所在组件的 VNode 更新时调用
  • componentUpdated : 指令所在的组件的 VNode 全部更新完成后
  • unbind : 指令与元素解绑时调用

钩子函数参数

  • el : 指令所绑定的元素 , 可以直接操作 DOM
  • binding : 指令相关的配置对象
    • modifiers : 一个包含修饰符的对象 示例 v-drag.limit
    • name : 指令名 , 不包含前缀
    • value : 指令绑定的值 v-drag="true"
<div v-drag>
// 拖拽方块案例
Vue.directive('drag', {
  // 初始化样式
  bind(el) {
    el.style.position = 'absolute'
    el.style.top = 0
    el.style.left = 0
    el.style.width = '100px'
    el.style.height = '100px'
    el.style.background = 'skyblue'
    el.style.cursor = 'pointer'
  },
  // 元素对象存在后, 开始写拖动逻辑
  inserted(el, binding) {
    let draging = false
    let elLeft = 0
    let elRight = 0
    
    document.addEventListener('mousedown', function (e) {
      draging = true
      let move = el.getBoundingClientRect()
      elLeft = e.clientX - move.left
      elRight = e.clientY - move.top  
    })
    document.addEventListener('mousemove', function (e) {
      let moveX = e.clientX - elLeft
      let moveY = e.clientY - elRight
      
      if (draging) {
        el.style.left = moveX + 'px'
        el.style.top = moveY + 'px'
      }
    })
    document.addEventListener('mouseup', function () {
      draging = false
    })
  }
})

自定义指令修饰符

相信大家仔细看上面的代码可能会发现这个方格拖拽还存在一些问题 ; 它还是可以拖拽到可视区域之外的 , 那么可不可以传递一个修饰符 , 来告诉他呢 ? 这时候就需要用到 binding 这个指令配置相关的对象了

// 我们先传入修饰符 limit 为自己定义的修饰符
<div v-drag.limit>
// 既然不想让他拖拽出视口, 那么就应该在鼠标移动的时候加入一些逻辑
document.addEventListener('mousemove', function (e) {
  let moveX = e.clientX - elLeft
  let moveY = e.clientY - elRight
  
  // 是否传入了修饰符 limit 为什么这样可以获取 下面就上截图
  if (binding.modifiers.limit) {
    moveX = moveX <= 0 ? moveX = 0 : moveX   
    moveY = moveY <= 0 ? moveY = 0 : moveY
  }
  if (draging) {
    el.style.left = moveX + 'px'
    el.style.top = moveY + 'px'
  }
  console.log(binding) // binding 对象
})

自定义指令传参

上面我们已经解决了拖出视口的问题 , 只要传递一个修饰符就解决了 , 那么现在我们希望可以手动的暂停拖拽 , 当然也是可行的 ;

<div v-drag.limit="{isDrag: false}">
document.addEventListener('mousemove', function (e) {
  let moveX = e.clientX - elLeft
  let moveY = e.clientY - elRight
  
  // 是否传入了修饰符 limit
  if (binding.modifiers.limit) {
    moveX = moveX <= 0 ? moveX = 0 : moveX   
    moveY = moveY <= 0 ? moveY = 0 : moveY
  }
  // 是否传入 isDrag 判断是否可滑动
  if (!binding.value.isDrag) return
  if (draging) {
    el.style.left = moveX + 'px'
    el.style.top = moveY + 'px'
  }
})

组件

通常一个组件会以一棵嵌套的组件数的形式来组织 ; 为了能在模板中使用 , 这些组件必须先注册以便 vue 能够识别 ;

  • 全局组件
Vue.component('GlobalComponent', {
  template: `<div> hello component </div>`
})
// 命名时推荐驼峰 , 调用时推荐 - 链接, html 不识别大小写
<global-component></global-component>
  • 局部组件
new Vue({
  el: '#app',
  components: {
    SonComponent: {
      template: `<div>hello private component</div>`
    }
  }
})
// 组件可以被复用多次
<private-component></private-component>
<private-component></private-component>
<private-component></private-component>
  • 模块化开发中的组件
import SonComponent from '@/components/SonComponent.vue'

export default {
  components: {
    SonComponent
  }
}
<son-component></son-component>

通过 props 向子组件传递数据

prop 是组件上一些自定义的 attribute , 当一个值传递给一个 prop attribute 的时候 , 它就变成那个组件实例的 property ;

// 父组件
<div>
  <son-component content="传递给子组件的数据, 如果动态传值可以加 v-bind"></son-component>
</div>
// 子组件
Vue.component('SonComponent', {
   // 多个单词可以是驼峰式, 但是父组件传递时多个单词必须是 - 连接
   // props 中的值, 可以像 data 中的数据一样访问  this.content / {{ content }} 
   // props 是只读的 切记不要修改 会报错
   props: ['content'],
   template: `<div> {{ content }} </div>`
})

props 可以是数组也可以是一个对象 , 用来接收来自父组件的数据 ;

对象允许配置高级选项 , 如类型检测等

  • type : 可以是 Number String Boolean Array Object Date Function 任何自定义构造函数 , 或上述内容组成的数组 , 会检查一个 prop 是否是给定的类型 , 否则抛出异常
  • default : 默认值 , 对象或者数组的默认值必须从一个工厂函数中返回
  • required : boolean 是否为必填项
  • validator : Function 自定义验证函数会将 prop 的值作为唯一的参数传入
props: {
  content: {
    type: String,
    // default: 0, 普通值可直接默认返还
    default: () => [1, 2, 3],
    required: true,
  	 // 如果传进来的 content 长度大于 20 就会报错
    validator: (value) => value.length >= 20
  }
}

监听子组件事件 $emit

有些时候 , 父组件需要用的子组件中特定的值时 , 可以使用 $emit 把这个值传递出去

  • 行内模式传值
// 子组件
<template>
  <div class="son">
    // $emit 第一个参数自定义事件, 第二个及以后是传递的数据
    <button @click="$emit(son-com, [1, 2, 3])"></button>
  </div>
</template>
// 父组件
// 监听子组件定义的自定义事件 , 通过 $event 访问第一个传递的参数
<son-component @son-com="msg = $event"></son-component>
  • 事件处理函数传值
// 子组件
<template>
  <div class="son">
    <button @click="sonHandle"></button>
  </div>
</template>
<script>
export default {
  methods: {
    sonHandle() {
      this.$emit('son-com', '需要传递的值')
    }
  }
}
</script>
// 父组件
<template>
  <div class="parent">
    <son-component @son-com="parentHandle"></son-component>
    // 显示传入其他参数的话 必须使用 $event 接收子组件传递过来的值
    <son-component @son-com="parent('显示传入参数', $event)"></son-component>
  </div>
</template>
<script>
export default {
  methods: {
    // 默认第一个值就是传递过来的参数
    parentHandle(arg) {
      	console.log(arg) // 需要传递的值
    },
    // 对应传入参数的位置
    parent(params, arg) {
      console.log(params) // 显示传入参数
      console.log(arg) // 需要传递的值
    }
  }
}
</script>
  • 事件处理函数传递多个值
// 子组件
<template>
  <div class="son">
    <button @click="sonHandle"></button>
  </div>
</template>
<script>
export default {
  methods: {
    sonHandle(event) {
      // 事件对象可以在任意位置 , 放到前面相对比较好接收
      this.$emit('son-com', event, '需要传递的值', '需要传递的第二个值')
    }
  }
}
</script>
// 父组件
<template>
  <div class="son">
    <button @son-com="parent"></button>
  </div>
</template>
<script>
export default {
  methods: {
    parent(event, ...args) {
      console.log(event)
      // 如果不想使用剩余参数, 也可以多传递参数逐个使用
      console.log(args)
    }
  }
}
</script>

组件上使用 v-model

在使用这个功能之前我们需要先了解一个东西 , v-model 究竟是什么 ; 其实它从某种程度来说就是一个语法糖

<input type="text" v-model="msg"/>

等价于

<input :value="msg" @input="msg = $event.target.value"/>

应用到组件中就是下面这样 为了不引起歧义, 我把自定义的事件以及属性加了test 前缀详情看官网

// 父组件
<model-input 
  :test-value="searchText" 
  @test-input="searchText = $event"
  >
</model-input> 
  • 子组件的 result 必须绑定到 value 上面
  • 在这个 input 触发的时候 通过 $emit 将自定义的 test-input 在暴露出去
// 子组件
<input
  type="text"
  v-bind:value="testValue"
  @input="$emit('test-input', $event.target.value)"
>
// script
props: ['testValue'] // 自定义属性传递过来的值

此时我们再优化一下 , 使用 v-model

// 父组件
<model-input v-model="searchText"></model-input> 

由于我们组件中使用了 v-model 而前面我们也提到了 v-model 其实是 v-bind 和 v-on 的语法糖 , 所以只能用 value 属性和 input 事件

// 子组件
<input
  type="text"
  v-bind:value="value"
  @input="$emit('input', $event.target.value)"
>
// script
props: ['value'] 

那么问题来了 , 上面我们提过 v-model 默认是 value属性 和 input事件 , 但是像单选框 , 复选框等类型怎么处理 ? 对此 Vue 提供了 model 选项来避免这样的冲突

单个复选框组件绑定

// 父组件
<model-input v-model="isChecked"></model-input>

// script
data() {
  return {
    isChecked: false
  }
},

选中 和 未选中 返回 true / false

// 子组件
<input
  type="checkbox"
  v-bind:checked="checked"
  @change="$emit('change', $event.target.checked)"
>
// script

export default {
  name: 'ModelInput',
  // v-model 拆分
  model: {
    prop: 'checked', // 将传进来的 isChecked 变成 checked 供后面的 props 使用
    event: 'change' //  定义 emit 自定义的事件名字
  },
  props: {
    checked: {
      type: Boolean
    }
  }
}

组件插槽

在 2.6.0 中 为具名插槽和作用域插槽提供了新的统一语法 v-slot 它取代了 slotslot-scope 这两个目前已被废弃 , 但是还没有移除 (仍然可以使用)

插槽 : 简单理解就是 占坑 在组件模板中占好位置 , 当使用该组件的标签时 , 组件标签的内容就会自动填坑 , ( 替换组件模板中的 slot 位置 ) , 并且可以作为承载分发内容的出口

内容插槽

// 子组件
<template>
  <div>
    <p>这是组件的头部</p>
    <slot></slot>
    <p>这是组件的尾部</p>
  </div>
</template>
// 父组件
<<template>
  <div>
    <!-- 插槽内可以是任何内容 组件,文本,标签-->
    <slot-test>
      <p>这是插槽的内容</p>
    </slot-test>
  </div>
</template>

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

默认内容插槽

<slot> 标签内可以加入 组件, 文本, 标签等默认内容 , 如果父组件调用时, 没有传入内容, 那么就会展示默认的内容

// 子组件
<template>
  <div>
    <p>这是组件的头部</p>
    <slot>我是默认内容</slot>
    <p>这是组件的尾部</p>
  </div>
</template>

具名插槽

有些时候一个插槽是不能满足需求的 , 我们可能需要多个 ; 对于这种情况 , <slot> 元素中有一个特殊的 attribute name 这个 attribute 用来定义额外的插槽

// 子组件
<template>
  <div>
    <header>
      <slot name="header"></slot>
    </header>
    <main>
      <!-- 如果没有指定 name 默认的 name 为default -->
      <slot></slot>
    </main>
    <footer>
      <slot name="footer"></slot>
    </footer>
  </div>
</template>

在向具名插槽提供内容的时候 , 可以在一个 template 元素中上 使用v-slot 指令 并以参数的形式提供名称

// 父组件
<template>
  <div>
	<slot-test>
	  <template v-slot:header> 我是 header 内容 </template>
      我是 没有指定 name 的内容 <!-- 或者也可以写成下面的内容 -->
      <template v-slot:default> 没有指定 name 的内容 </template>
      <template v-slot:footer> 我是 footer 内容 </template>
	</slot-test>
  </div>
</template>

<template> 中的所有内容都会传入响应的插槽 , 任何没有包裹在带有 v-slot<template> 中的内容都会被视为默认插槽的内容 ; 通俗一点来说 , 就是我在子组件中声明了多个 没有 name<slot> , 那么 我在父组件中 , 只需要渲染一次 , 所有子组件的插槽就都会被渲染 ;

注意 : v-slot 只能添加在 <template> 上 , 还有一种特殊情况后面会说

具名插槽缩写

v-slot: 替换为 #

注意 : 只有带参数时可以使用 , 其他情况下是无效的

// 子组件
<div>
  <slot></slot>
</div>
/* 父组件 */

// 错误示例
<slot-test>
  <template #>
    <!-- 内容。。。。-->
  </template>
</slot-test>

// 正确示例
<slot-test>
  <template #default>
    <!-- 内容。。。。-->
  </template>
</slot-test>

作用域插槽

从某种意义上来说 , 插槽是子组件高可用 , 高定制化的一种手段 , 那么我们肯定会碰到插槽内容 , 需要访问子组件中数据的情况 ;

我们可以通过 v-bind 把需要传递的值 绑定到 <slot> 上 , 然后在 父组件中 使用 v-slot 设置一个值来定义提供插槽的名字

// 子组件
<template>
  <div>
	<slot name="userInfo" :user="userInfo"></slot>
  </div>
</template>
<script>
export default {
  data() {
    return {
      userInfo: {
        firstName: 'firstName',
        lastName: 'lastName'
      }
    }
  }
}
</script>
// 父组件
<template>
  <div>
    <slot-test>
      <!-- 如果只有一个插槽可以吧 v-slot 写到 组件上面 具体看下面-->
      <template v-slot:userInfo="slotProps">
        {{ slotProps.user.firstName }}
      </template>
    </slot-test>
  </div>
</template>

独占默认插槽缩写语法

当被提供的内容只有默认插槽时 , 组件的标签才可以当做插槽的模板使用 , 这样我们就可以把 v-slot 直接用在组件上

// 子组件
<div>
  <slot :user="userInfo"></slot>
</div>

// 组件数据
data(){
  return {
    userInfo: {
      firstName: 'firstName',
      lastName: 'lastName'
    }
  }
}
// 父组件
<div>
  <slot-test v-slot="slotProps">
    {{ slotProps.user.firstName }}
  </slot-test>
</div>
  • 不要尝试插槽的 "缩写语法" 和 "具名插槽混用" 会导致作用域不明确
  • 只要出现多个插槽 , 需要始终为所有的插槽使用 <template> 语法
  • 参考官网 : 默认插槽

解构插槽 Prop

插槽支持通过 ES6 结构传入具体的插槽 prop 至于原理 ;

官网说 : 作用域插槽内部原理是将你的插槽内容包括在一个传入单个参数的函数里 ; 没有听懂 😂 自身档次还不够, 还是看看怎么用的吧

<div>
  <slot-test v-slot="{ user }">
    {{ user.firstName }}
  </slot-test>
</div>

在提供多个 prop 的时候 ,它同样开启了 prop 重命名的功能

<div>
  <slot-test v-slot="{ user: preson }">
    {{ preson.firstName }}
  </slot-test>
</div>

插槽其他示例

插槽 prop 允许我们将插槽转换为可复用的模板 , 这些模板可以基于输入不同的 prop 渲染出不同的内容;

比如我们设计一个 <todo-list> 组件 ,它是一个列表且包含一定的逻辑

// 子组件
<template>
  <ul>
    <li v-for="item in list" :key="item.id">
      <!-- 这里把控制逻辑交出去,由父组件去控制一定的逻辑 -->
      <slot name="todo" :item="item">
        {{ item.uname }}
      </slot>
    </li>
  </ul>
</template>
// 数据
[
  {id: 1, uname: 'zs'},
  {id: 2, uname: 'ls'},
  {id: 3, uname: 'we'},
  {id: 4, uname: 'mz'},
]
// 父组件
<test v-slot:todo="slotProps">
  <!-- 本来是显示名字的改成了显示id -->
  {{ slotProps.item.id }}
</test>

动态组件 component

在说动态组件之前, 先动手实现一个 tab 分页,不然貌似不是很好理解 😂

// 模板
<div>
  <button @click="handle('post')" :style="{background: !isShow ? 'red' : ''}">post</button>
  <button @click="handle('list')" :style="{background: isShow ? 'red' : ''}">list</button>
  <item-post v-show="!isShow"></item-post>
  <item-list v-show="isShow"></item-list>
</div>

// script
import ItemPost from '@/components/ItemPost.vue'
import ItemList from '@/components/ItemList.vue'

export default {
  name: 'Home',
  components: {
    ItemPost,
    ItemList
  },
  data() {
    return {
      isShow: true
    }
  },
  methods: {
    handle(title) {
      title === 'list' ? this.isShow = true : this.isShow = false
    }
  }
}

可以看出上面我们写了两个小组件,做了一个简易的 tab 选项卡 ;

重点可以看我们引入组件的方式;就是简单的在模板中应用;常规操做;现在只引入两个标签还好,那如果三五十来个就显得有点麻烦了

动态组件应用

有了动态组件之后, 类似这种简易的 tab 切换我们就不需要再模板中使用这么多的组件标签了

看代码示例, 我们只需要 template 中引入组件的部分 以及在 script 中的 data 新增一个属性

<!-- 每次点击 button 的时候触发 handle事件函数时 is绑定的值会重新渲染组件 -->
<component :is="showWhat"></component>
data() {
  return {
    showWhat: 'ItemPost',
    isShow: true
  }
},
methods: {
  handle(title) {
    // 这里同时控制了切换之后 active 的样式,所以就不删了
    title === 'list' ? this.isShow = true : this.isShow = false
    // 加一层判断切换逻辑
    title === 'list' ? this.showWhat = 'ItemList' : this.showWhat = 'ItemPost'
  }
}

这里就出现了一些问题, 虽然我们完成了 组件之间的切换,但是关于 input 的选中状态我们没有保留下来,原因是每次切换的时候,Vue 都会创建一个新的 showWhat 实例 重新创建动态组件的行为通常来说还是非常有用的,但是在我们这个案例中,更希望它能够保留下来 为了解决这个问题,Vue 中提供了一个内置元素 <keep-alive> 只需要将该元素把组件包裹起来即可

<!-- 失活的 tab 将会被缓存 -->
<keep-alive>  
  <component :is="showWhat"></component>
</keep-alive>

异步组件

在大型应用中, 我们可能需要将应用中分割成小一些的代码块,并且只在需要的时候,再次加载这个模块;道理和 webpack 的按需加载是一样的;

这里先创建一个简单的组件

<template>
  <div>这是一个简单的组件</div>
</template>

然后在主组件内常规引入看下效果

<template>
  <div>
    <dynamic></dynamic>
  </div>
</template>
<script>
import Dynamic from '@/component/Dynamic.vue'

export default {
  name: 'Home',
  components: {
    Dynamic
  }
}
</script>

看下图, 上面的书写方式,加载出来的组件,会被全部渲染到一个文件里面,如果我们的这个页面组件很多,或者说有些组件,只有触发了特定操作后才会显示,我们是不是就没有必要在页面渲染时,就把所有的组件全部加载呢 ?

异步组件示例

根据上面的例子,我们修改一下 script 部分

export default {
  components: {
    // script 头部的导入, 直接在组件内导入
    Dynamic: () => import Dynamic from '@/component/Dynamic.vue'
  }
}

可以看出下图多出了 0.js 由此可见 vue-cli 帮助我们分开打包了文件;这个文件在被加载之后,就会被缓存起来;这个例子只是看出了它被拆开了,并不能证明咱们开始说的按需渲染的道理;基于这个例子可以再改一下,加一个判断逻辑

<template>
  <div>
    // 就是增加一个按钮,点击控制,这里就给大家简化了
    <dynamic  v-if="isShow"></dynamic>
  </div>
</template>

下图可以看出没有显示子组件时, 把这个文件给缓存起来了,只有真正调用到的时候才会加载对应的资源;

当然这只是其中一个功能,比如网络不好的时候,组件加载的用户体验会受到损害,超时的处理等等;具体配置参考 vue 官网 : 处理加载状态

访问子组件实例或子元素 ref

尽管存在 prop 和事件 , 有的时候你扔可能需要再 JavaScript 中直接访问一个子组件 , 为了达到这个目的你可以使用 ref 这个属性为子组件赋予一个 id 引用

// 模板
<template>
  <div>
    <item-list ref="itemRef"></item-list>
    <input type="text" ref="inputRef"/>
  </div>
</template>
// script
export default {
  mounted() {
    // 子组件的实例对象
    console.log(this.$refs.itemRef)
    // 元素的实例对象, 可以操作 dom
    console.log(this.$refs.inputRef)
  }
}

如果 ref 和 v-for 一起使用的话 , 那么 this.refs 将会得到一个元素数组

<ul>
  <li v-for="user of userList" :key="user.id" :style="{listStyle: 'none'}" ref="forRef">
    <input type="checkbox" />
    {{ user.id }} --- {{ user.uname }} --- {{ user.age }}
  </li>
</ul>

mounted() {
  console.log(this.$refs.forRef)
}

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

组件生命周期

每个 Vue 实例创建时都需要经过一系列的初始化 ; 开始创建 , 初始化数据 , 编译模板, 挂载DOM , 更新 , 渲染 , 卸载等一系列过程 , 成为 生命周期 , 同时在这些过程中也会运行一些叫做生命周期钩子的函数

  • beforeCreate : 创建前 实例初始化之后,this指向创建的实例,不能访问到datacomputedwatchmethods上的方法和数据

  • created : 创建后 实例创建完成 , 可以访问 data computed watch methods 数据 , 没有渲染进 浏览器 无法访问 DOM ; 注意 : 这个生命周期内发送 ajax 请求 是没有什么方法对实例化过程进行拦截的 , 因此加入某些数据必须获取之后才能进入这个页面的话 , 并不适合在这个方法内完成 , 建议使用 beforeRouterEnter 路由钩子中完成

  • beforeMount : 挂载前 挂载开始之前调用 , beforeMount 之前会找到对应的 template 编译成 render 函数

  • mounted : 挂载后 实例挂载到 DOM , 可以操作DOM $ref 可以使用 ; 此时可以做一些 ajax 操作 , mounted 只会执行一次

  • beforeUpdate : 更新前 响应式数据更新时调用 , DOM 重新渲染和打补丁之前 , 可以在这里进一步更改状态 , 不会进行二次渲染

  • updated : 更新后 虚拟DOM重新渲染和打补丁之后调用 , 组件已经更新 , 可以执行后续操作 ; 避免在这里操作数据 , 可能会陷入死循环

  • activated : 被 keep-alive 缓存的组件激活时调用

  • deacticated : 被 keep-alive 缓存的组件停用时调用

// 子组件
// 要在子组件的声命周期内 , 才会触发这两个钩子函数
activated() {
  console.log('itemList 被激活')
},
deactivated() {
  console.log('itemList 被停用 / 失活')
},
// 父组件
// toggle 切换时触发子组件中的这两个钩子函数
<button @click="com = 'ItemPost'">post</button>
<button @click="com = 'ItemList'">list</button> 

<keep-alive>
  <component :is="com"></component>
</keep-alive>
  • beforeDestroy : 销毁前 实例销毁之前调用 , 这里各种数据仍然可以访问 ; 可以再次销毁定时器, 解绑事件等操作
  • destroyed : 销毁后 实例销毁后调用 , Vue 实例的所有内容都会解除绑定 , 所有事件事件监听器会被移除 , 所有子实例也会被销毁
<template>
  <div>
    <p>this is $el</p>
    <button @click="info = '修改后的值'">{{ info }}</button> 
    <button @click="handle">卸载</button> 
  </div>
</template>
<script>
export default {
  name: 'HomePage',
  data() {
    return {
      info: 'data options',
      flag: true
    }
  },
  methods: {
    handle() {
      this.$destroy()
    }
  },
  beforeCreate() {
    console.group('##### 组件创建前 beforeCreate #####')
    console.log('Vue', this)
    console.log(this.info)
    console.log(this.$el)
  },
  created() {
    console.group('##### 组件创建后 created #####')
    console.log(this.info)
    console.log(this.$el)
  },
  beforeMount() {
    console.group('##### 组件挂载前 beforeMount #####')
    console.log(this.info)
    console.log(this.$el)
  },
  mounted() {
    console.group('##### 组件挂载后 mounted #####')
    console.log(this.$el)
  },
  beforeUpdate() {
    console.group('##### 组件更新前 beforeUpdate #####')
    console.log(`这里的数据已经修改了只是没有渲染 ----- `+ this.info)
    this.info  = '又修改了一次'
  },
  updated() {
    console.group('##### 组件更新后 updated #####')
    console.log('更新后的新值: ', this.info)
  },
  beforeDestroy() {
    console.group('##### 组件卸载前 updated #####')
    console.log(this.info)
    console.log(this.$el)
  },
  destroyed() {
    console.group('##### 组件卸载后 updated #####')
    console.log(this.info)
    console.log(this.$el)
  }
}
</script>

父子组件生命周期

  • 父组件执行完 beforeMount 钩子之后就会去加载 子组件 , 只组件加载完成后才会触发父组件的 Mounted
  • 子组件更新不会触发父组件的更新, ( 不涉及父子组件交互数据 )
  • 子组件的卸载会触发父组件的更新

过渡 动画

Vue 在插入,更新或者移出 DOM 时,提供多种不同的过渡效果

在下列情形中可以给任何元素添加进入/离开过渡

  • 条件渲染 (v-if v-show
  • 动态组件 (component)
  • 组件根节点

过渡的类名:在进入/离开的过渡中,会有6个 class 切换

  • v-enter : 定义过渡的开始 , 在元素被插入之前生效 , 在元素被插入之后的下一帧移除
  • v-enter-active : 定义进入过渡生效时的状态 , 在整个进入过渡的阶段中都会应用 . 在元素被插入之前生效 . 咋过渡/动画结束之后移除 , 这个类可以被定义进入过渡的过程时间 , 延迟 和曲线函数
  • v-enter-to : 定义进入过渡的结束状态 , 在元素被插入后的下一帧生效 ( 同时 v-enter 移除 ), 在过渡动画完成之后移除
  • v-leave : 定义离开过渡的开始时间 , 在离开过渡被触发时立刻生效 , 下一帧被移除
  • v-leave-active : 定义离开过渡生效时的状态 , 在整个离开过渡的阶段中应用 , 在离开过渡被触发时 立即生效 , 在过渡动画完成之后移除 , 这个类可以被定义离开过渡的过程时间 , 延迟和曲线函数
  • v-leave-to : 定义离开过渡的结束状态 , 在离开过渡被触发后下一帧生效 (同时 v-leave 移除), 在过渡东环完成之后移除

<transition>
  <dynamic v-if="isShow"></dynamic>
</transition>

如果 <transition> 没有 name 属性 类名默认是 v- 开头 , 如果使用了自定义名字 , 替换成自定名字开头 ; transition 不会被渲染成真是的 DOM 元素

示例 : <transition name="my-trans"> my-trans-enter

<style lang="css">
  .v-enter, .v-leave-to{
    opacity: 0;
    transition: all .5s ;
  }
  .v-enter-to, .v-leave{
    opacity: 1;
    transition: all .5s ;
  }
</style>

多个组件的过渡

<transition>
  <keep-alive>  
    <component :is="showWhat"></component>
  </keep-alive>
</transition>
<style lang="css">
  .v-enter, .v-leave-to{
    opacity: 0;
  }
  .v-enter-active, .v-leave-active{
    transition: opacity .3s ease;
  }
</style>

过渡模式

在切换 tab 的时候 内容被重绘了 , 一个是离开过渡的时候, 另一个是进入过渡 , 这是 <transition> 的默认行为 , 进入和离开同时发生 ; 过渡模式仅适用于组件之间

同时生效的进入和离开的过渡不能满足所有要求,所以 Vue 提供了过渡模式

  • in-out : 新元素先进行过渡 , 完成之后当前元素过渡离开
  • out-in : 当前元素先进行过渡 , 完成之后新元素过渡进入

可以看出上图的动画效果为当前元素还没有完成 , 新元素就进来了 , 此时我们可以使用 out-in 模式即可解决上述问题

<transition mode="out-in">
  <keep-alive>  
    <component :is="showWhat"></component>
  </keep-alive>
</transition>

初始渲染过渡

可以通过 appear 属性设置节点的初始渲染过渡 ; 需要注意的是, 这个关键字加上之后, 默认就会带有过渡效果 , translateY 也可以自定义类名和钩子 ; 初始渲染过渡

<transition appear>
  <div v-if="toggle">post</div>
  <div v-else>list</div>
</transition>

列表过渡

列表过渡需要用到 <transition-group>

  • 会产生标签名, 默认为 span 可以通过 tag 属性进行设置
  • 过渡模式不可用
<transition-group  tag="ul" appear>
  <li v-for="user of users" :key="user.id">
    <p>{{ user.uname }}</p>
  </li>
</transition-group>

更多动画相关 : Vue过渡动画

nextTick

Vue 在更新 DOM 的时候是异步的 , 只要侦听到数据的变化 , 并缓存在同一事件循环中 , 等待事件循环中的所有的数据变化完成后, 统一更新视图 , 为了得到最新的DOM 所以设置了 nextTick()

将回调延迟到下次 DOM 更新循环之后执行 , 在修改数据之后立即使用他 , 然后等待 DOM 更新 ;

简单理解就是 : nextTick 是将这个回调函数延迟在下一次 DOM 更新数据后调用 , 即 DOM 重新渲染后自动执行该函数

created() {
  // created 钩子中可以操作 DOM 元素
  this.$nextTick(() => {
    this.$refs.divRef.innerHTML = 'hello vue'
  })
}

混入 Mixin

混入提供一种很灵活的方式, 来分发 Vue 组件中的可复用功能, 一个混入对象可以包含任意组件选项 (data, components, methods, created 等), 当组件使用混入对象时, 所有混入对象的选项将被混合进组件本身的选项 ;

假设我们现在要封装一个 button 组件, 我们可以尝试着不再 components 文件夹中去封装, 在同级目录中新建一个 Mixin 文件夹 实例

<!-- path: Mixin/MyButton/MyButton.vue -->
<template>
  <button :class="['my-btn', type]">
    <!-- 我们希望用户输入按钮的名字所以使用插槽 -->
    <slot></slot>
  </button>
</template>
<script>
// 我们希望用户传递进来一个 type 来定制 btn 样式
export default {
  props: {
    type: {
      type: String,
      default: 'my-primay'
    }
  }
}
</script>
<style scoped>
 /* 基类样式 */
 .my-btn {
    border: none;
    outline: none;
    line-height: 30px;
    width: 50px;
  }
 /* 用户可拓展样式 */
.btn-primary {
  background: skyblue;
}
.btn-danger {
  background: orange;
}
.btn-success {
  background: palegreen;
}
</style>

然后我们在同级别的目录下建立一个 Mixin 对象

// path: Mixin/MyButton/index.js
import MyButton from './MyButton.vue'
export default {
  // 这里我们使用了 组件中的 components 对象
  components: {
    MyButton
  }
}

然后我们在页面中去使用它

<!-- views/page.vue -->
<template>
  <!-- 此时我们只需要传入指定不同的类名即可变换 button 的样式 -->
  <my-button type="btn-danger">按钮</my-button>
</template>
<script>
import MyButton from './mixin/MyButton'
export default {
  // 组件内使用 mixins 将这个对象混入, 
  mixins: [MyButton]  
}
</script>

简单总结一下, 可以看出我们上面虽然用的有些牵强, 不晓得有没有注意到 , 我们实际上等于把 注册组件的方式给抽离出去了, 取而代之的是把他混入进去, 这里就可以简单理解, 很想 合并了两个对象的概念 , 那么接下来就看一下细节

选项合并

当组件和混入对象含有同名选项时 , 这些选项将以恰当 的方式进行合并

什么叫恰当 😂 看示例

  • 输入对象会进行递归合并 , 并在发生冲突时,以组件数据优先
  • 同名的钩子函数会合并成为一个数组,依次调用,mixin的钩子优先执行
  • 对象类型选项会合并为同一个对象,如果键名冲突,以组件内为主
// mixin/index.js
export default {
  data() {
    return {
      m_name: 'lisi',
      m_list: [1, 2, 3]
    }
  },
  created() {
    // 混入之后会被调用 同时我们也可以在混入之前调用组件内的数据
    console.log(this.m_name, this.m_list, 'mixin')
    console.log(this.c_name, this.c_list, 'component')
      
    // 执行 handle 
    this.handle() // 这个handle 实际就是调用了 组件中的handle 而不是 mixin 中的handle
    this.handle1()
  },
  methods: {
    // 由于此处的 handle 与 组件内存在同名 按照上面说明, 这个handle被覆盖以下面组件中为主
    handle() {
      console.log('mixin handle')
    },
    handle1() {
      console.log('mixin handle1')
    }
  }
}
<template>
  <div></div>
</template>
<script>
import mixin from '@/mixin'
export default {
  mixins: [mixin],
  data() {
    return {
      c_name: 'zhangsan',
      c_list: [1, 2, 3]
    }
  },
  created() {
    // 同理这里也可以直接访问 mixin 中的数据,这里就不演示了
    console.log('this is component')
      
    // 调用handle 
    this.handle() 
  },
  methods: {
    handle() {
      console.log('component handle')
    }
  }
}
</script>

全局混入

混入也可以全局注册 , 使用时需要小心, 全局混入会影响每一个之后创建的 vue 组件,也就是说以后创建的每一个 Vue 组件会混入这个 mixin 都会执行这里面的操作

// main.js
Vue.mixin({
  data() {
    return {}
  },
  created() {},
  // ......
})

provide / inject 依赖注入

provideinject 主要开发高阶插件/组件库时使用,并不推荐用于普通应用程序中

这对选项要一起使用,允许一个祖先组件向自己所有的子孙组件注入依赖,不论组件层间有多深;

也就说这对选项可以跨越组件的传值 下面看示例

// App.vue
<template>
  <div>
    <router-view></router-view>
  </div>
</template>
<script>
export default {
  data() {
    return {
      info: { name: 'zs', age: 23 }
    }
  },
  // 使用 provide 把值传递出去
  // 这里碰到一点小问题,官网说可以是 Objct | function 返回一个对象,这里试了 Object 就是不能成功不知道为啥😭
  provide() {
    return {
      info: this.info,
      reload: this.reload
    }
  },
  methods: {
    reload() {
      console.log('reload')
    }
  }
}
</script>
// Home.vue
<template>
  <div>
    Home
    <provide-test></provide-test>
  </div>
</template>
<script>
// 这里我们只是把需要用到 provide 数据的组件引入,可以看出我们并没有使用
import ProvideTest from '@/components/ProvideTest.vue'
export default {
  components: {
    ProvideTest
  }
}
</script>
// components/ProvideTest.vue
<template>
  <div></div>
</template>
<script>
export default {
  // 这里我们使用 indect 来接收 父组件传递过来的数据,可以看出这种方式类似于 props 接收
  inject: ['info', 'reload'],
  mounted() {
    // 然后就可以通过实例访问这些数据了
    console.log(this.info)
    this.reload()
  }
}
</script>

当然 inject 也不是只能通过数据来接收参数;具体参考 cn.vuejs.org/v2/api/#pro…

provideinject 绑定并不是响应式的, 这是刻意为之,如果你传入了一个可监听的对象,那么其对象的 property 还是可响应式的; inject 接收过来的数据,不要试图修改,会报错的

非父子组件传值 on/on / emit

$emit 这个咱们前面就知道了 触发当前实例上的事件, 附件参数传给监听器的回调 (可以实现子组件向父组件间的传值)

了解如何传值之前先了解 $on

$on 监听当前实例的上的自定义事件,事件可以由 $emit 触发,回调函数会接收所有传入事件触发的额外参数, 乍一看和指令 v-on 差不多,那么下面就看下具体示例

// About.vue
<template>
  <div>
    <button @click="handle">点击传递</button>
  </div>
</template>
<script>
export default {
  methods: {
    handle() {
      // 这里使用 emit 触发一个自定义事件 如果是监听原生事件 同样可以传递 event
      this.$emit('on-click', '单文件内被监听了')
    }
  },
  mounted() {
    // 在 mounted 中 用 on 去监听触发的事件, 并且接收传递过来的参数
    this.$on('on-click', msg => {
      console.log(msg)
    })
  }
}
</script>

当然如果只是这样使用, 肯定是略显鸡肋, 既然知道 API 的特性,那么下面看父子组件传值

兄弟组件之间的通信,EventBus Vue 中也叫 事件总线,是一种发布订阅模式关于发布订阅模式)通过 EventBus 来作为沟通桥梁,让所有组件共用一个事件中心,可以像该事件中心注册事件或者接收事件

// Bus/index.js
import Vue from 'vue'

// 创建一个空的 Vue 实例 用来做 桥梁
export const Bus = new Vue()
// main.js
import { Bus } from './Bus'
// 将这个空的实例绑定到 vue 的原型中
Vue.prototype.$bus = Bus
// About.vue
<template>
  <div>
    <button @click="handle">点击传递</button>
  </div>
</template>
<script>
export default {
  data() {
    return {
      message: ''
    }
  },
  methods: {
    handle() {
      // 通过 $bus.$emit 触发事件 可以是自定义事件也可以是原生事件
      this.$bus.$emit('click', '其他文件内被监听了', event)
    }
  }
}
</script>
// Home.vue
<template>
  <div></div>
</template>
<script>
export default {
  mounted() {
    // 兄弟组件中可以通过 $bus.$on 监听触发的事件, 并且接收一个回调处理参数
    this.$bus.$on('click', (msg, e) => {
      console.log(msg)
      console.log(e)
    })
  }
}
</script>

注意: 组件挂在之前会执行 mounted 钩子,所以在这个钩子中监听触发的事件,当然在 created 中也是可以的 建议还是 mounted