Vue Day5

250 阅读8分钟

组件的三大组成部分

注意点

  • 结构:只能有一个根元素
  • 样式:
    • 全局样式(默认):影响所有组件
    • 局部样式:scoped下样式,只作用于当前组件
  • 逻辑: el 根实例独有,data是一个函数,其他配置项一致

scoped 样式冲突

默认情况 :写在组件中的样式会全局生效——因此很容易造成多个组件之间的样式冲突问题

  1. 全局样式 : 默认组件中的样式会作用到全局,任何一个组件中都会受到此样式的影响

  2. 局部样式 : 可以给组件加上 scoped 属性,可以 让样式只作用于当前组件

scoped 原理:

  1. 给当前组件模板的所有元素都会添加一个自定义属性 data-v-hash值

  2. 利用hash值不同可以区分不同组件

  • hash值相同:在同一个组件范围之内
  • hash值不同:在不同组件范围之内 BaseOne.vue
  1. css选择器后面,被自动处理添加上了属性选择器 div[data-v-5f6aa9d56]

BaseTwo.vue 代码

<template>
  <div class="base-one">
    BaseTwo
  </div>
</template>

<script>
export default {

}
</script>

<style scoped>
</style>

App.vue

<template>
  <div id="app">
    <BaseOne></BaseOne>
    <BaseTwo></BaseTwo>
  </div>
</template>

<script>
import BaseOne from './components/BaseOne'
import BaseTwo from './components/BaseTwo'
export default {
  name: 'App',
  components: {
    BaseOne,
    BaseTwo
  }
}
</script>

data是一个函数

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

  • 目的:为了保证每个组件实例,维护 独立 的一份 数据 对象。

  • 每次创建新的组件实例,都会新执行一次data 函数,得到一个新对象。

BaseCount.vue中代码:

<template>
  <div class="base-count">
    <button @click="count--">-</button>
    <span>{{ count }}</span>
    <button @click="count++">+</button>
  </div>
</template>

<script>
export default {
  data: function () {
    return {
      count: 100,
    }
  },
}
</script>

<style>
.base-count {
  margin: 20px;
}
</style>

App.vue 中代码:

<template>
  <div class="app">
    <BaseCount></BaseCount>
  </div>
</template>

<script>
import BaseCount from './components/BaseCount'
export default {
  components: {
    BaseCount,
  },
}
</script>

<style>
</style>
image.png

组件通信

什么是组件通信?

组件通信就是指组件与组件之间的数据传递

  • 组件的数据是独立的,无法直接访问其他组件的数据。
  • 想使用其他组件的数据,就需要组件通信
1682308903094.png

不同的组件关系 和 组件通信方案分类

组件关系分类
  1. 父子关系(直接包含) props& $emit
  2. 非父子关系(没有关联)provide & inject 或 eventbus
1682318070876.png
通信解决方案
1682318111090.png
父子通信流程图
1682318444566.png

父——>子

父组件通过 props 将数据传递给子组件

1682318711785.png

父组件通过 props 将数据传递给子组件

props:就是给组件标签身上新增的自定义属性

  1. 给组件添加标签,通过添加属性的方式传值
  2. 通过props进行接收
1682318711785.png

子——>父

  1. 子组件利用$emit通知父组件,进行修改更新
  2. 父组件添加监听
  3. 提供处理函数,提供逻辑

1682318965635.png

Props 详解

Props 定义

Props 定义: 组件上注册的一些自定义属性

Props 作用:向子组件传递数据

特点:

  • 可以 传递 任意数量 的prop

  • 可以 传递 任意类型 的prop

父组件

App.vue中代码

<template>
  <div class="app">
    <UserInfo
      :username="username"
      :age="age"
      :isSingle="isSingle"
      :car="car"
      :hobby="hobby"
    ></UserInfo>
  </div>
</template>

<script>
import UserInfo from './components/UserInfo.vue'
export default {
  data() {
    return {
      username: '小帅',
      age: 28,
      isSingle: true,
      car: {
        brand: '宝马',
      },
      hobby: ['篮球', '足球', '羽毛球'],
    }
  },
  components: {
    UserInfo,
  },
}
</script>

<style>
</style>

子组件

UserInfo.vue中代码

<template>
  <div class="userinfo">
    <h3>我是个人信息组件</h3>
    <div>姓名:{{username}}</div>
    <div>年龄:{{age}}</div>
    <div>是否单身:{{isSingle}}'是''否'</div>
    <div>座驾:{{car,brand}}</div>
    <div>兴趣爱好:{{hobby.join('、')}}</div>
  </div>
</template>

<script>
export default {
  
}
</script>

<style>
.userinfo {
  width: 300px;
  border: 3px solid #000;
  padding: 20px;
}
.userinfo > div {
  margin: 20px 10px;
}
</style>

效果如下:

image.png

Props 校验

作用:组件的 prop 指定 验证要求,不符合要求,控制台就会有 错误提示 → 帮助开发者,快速发现错误

语法:

  • 类型校验(最常用)
  • 非空校验
  • 默认值
  • 自定义校验

1. 基础写法(类型校验)

  • 语法:
props:{
校验的属性名:类型//Number String Boolean……
//属性名:键  类型:值
},

示例:

App.vue中代码

<template>
  <div class="app">
    <BaseProgress :w="width"></BaseProgress>
  </div>
</template>

<script>
import BaseProgress from './components/BaseProgress.vue'
export default {
  data() {
    return {
      width: 30,
      //通过改变数值来改变进度条宽度
    }
  },
  components: {
    BaseProgress,
  },
}
</script>

<style>
</style>

BaseProgress.vue中代码

<template>
  <div class="base-progress">
    <div class="inner" :style="{ width: w + '%' }">
      <span>{{ w }}%</span>
    </div>
  </div>
</template>

<script>
export default {
 // props: ['w']
 props:{
   w:Number
   //若此时传入一个string (abc)则会报错
 }
}
</script>

<style scoped>
.base-progress {
  height: 26px;
  width: 400px;
  border-radius: 15px;
  background-color: #272425;
  border: 3px solid #272425;
  box-sizing: border-box;
  margin-bottom: 30px;
}
.inner {
  position: relative;
  background: #379bff;
  border-radius: 15px;
  height: 25px;
  box-sizing: border-box;
  left: -3px;
  top: -2px;
}
.inner span {
  position: absolute;
  right: 0;
  top: 26px;
}
</style>

效果图如下:

App.vue中传了width值,通过父传子的方式在BaseProgress.vue 中通过props接收,作用于div的盒子

image.png

数字和字符串可以直接default,但如果是数组和对象必须通过工厂函数返回默认值

2.完整写法(类型非空默认自定义)

props: {
  校验的属性名:{
    type:类型,  //Number String Boolean…
    required:true//是否必填
    default:默认值,//默认值
    validator(value){
    //自定义校验逻辑
    return 是否通过校验
    }
  }
},

接上一个示例(其他部分都相同):

BaseProgress.vue中代码:

<script>
   props: {
     w:{
     type:Number 
     //required:true
     default:0
     validator(value){
     //cosole.log(value)
     if (value >=0 &&value <=100){
     return true
     }else{
     console.error('传入的prop w,必须是0-100的数字')
     return false
    }
  }
},
</script>

注意:

1.default和required一般不同时写(因为当时必填项时,肯定是有值的)

2.default后面如果是简单类型的值,可以直接写默认。如果是复杂类型的值,则需要以函数的形式return一个默认值

prop &data 单向数据流

1.共同点

都可以给组件提供数据

2.区别

  • data 的数据是 自己 的 → 随便改
  • prop 的数据是 外部 的 → 不能直接改,要遵循 单向数据流
  • 谁的数据谁负责

3.单向数据流:

父级props 的数据更新,会向下流动,影响子组件。这个数据流动是单向的

1682323734228.png

prop是只读的,不允许修改

小黑记事本-组件版

需求说明

  • 拆分基础组件
  • 渲染待办任务
  • 添加任务
  • 删除任务
  • 底部合计 和 清空功能
  • 持久化存储

拆分组件

小黑记事本原有的结构拆成三部分内容:头部(TodoHeader)、列表(TodoMain)、底部(TodoFooter)

1682325598418.png

渲染功能:

  1. 提供数据-提供在公共父组件 App.vue
  2. 通过父传子,将数据传递给TodoMain
  3. 利用v-for 渲染

添加功能

  1. 收集表单数据 v-model
  2. 监听时间 (回车+点击 都要进行添加)
  3. 子传父,将任务名称传递给父组件App.vue
  4. 父组件接收到数据后 进行添加 unshift(自己的数据自己负责)
<input
   @keyup.enter="handleAdd"
   //回车事件
   v-model="todoName"placeholder="">

删除功能

  1. 监听事件(监听删除的点击) 携带id
  2. 子传父,将删除的id传递给父组件App.vue
  3. 进行删除 filter(自己的数据自己负责)

底部功能及持久化存储

  1. 底部合计:父组件传递list到底部组件 —>展示合计
  2. 清空功能:监听事件 —> 子传父 子组件通知父组件 —>父组件进行清空操作
  3. 持久化存储:atch深度监视list的变化 -> 往本地存储 ->进入页面优先读取本地数据
TodoHeader
<template>
   <!-- 输入框 -->
  <header class="header">
    <h1>小黑记事本</h1>
    <input placeholder="请输入任务" class="new-todo" v-model="todoName" @keyup.enter="handleAdd"/>
    <button class="add" @click="handleAdd">添加任务</button>
  </header>
</template>

<script>
export default {
  data(){
    return {
      todoName:''
    }
  },
  methods:{
    handleAdd(){
      // console.log(this.todoName)
     
      this.$emit('add',this.todoName)
      this.todoName = ''
    }
  }
}
</script>

<style>

</style>
TodoMain
<template>
  <!-- 列表区域 -->
  <section class="main">
    <ul class="todo-list">
      <li class="todo" v-for="(item, index) in list" :key="item.id">
        <div class="view">
          <span class="index">{{ index + 1 }}.</span>
          <label>{{ item.name }}</label>
          <button class="destroy" @click="handleDel(item.id)"></button>
        </div>
      </li>
    </ul>
  </section>
</template>

<script>
export default {
  props: {
    list: {
      type: Array,
    },
  },
  methods: {
    handleDel(id) {
      this.$emit('del', id)
    },
  },
}
</script>

<style>
</style>

TodoFooter
<template>
  <!-- 统计和清空 -->
  <footer class="footer">
    <!-- 统计 -->
    <span class="todo-count"
      >合 计:<strong> {{ list.length }} </strong></span
    >
    <!-- 清空 -->
    <button class="clear-completed" @click="clear">清空任务</button>
  </footer>
</template>

<script>
export default {
  props: {
    list: {
      type: Array,
    },
  },
  methods:{
    clear(){
      this.$emit('clear')
    }
  }
}
</script>

<style>
</style>
App.vue
<template>
  <!-- 主体区域 -->
  <section id="app">
    <TodoHeader @add="handleAdd"></TodoHeader>
    <TodoMain :list="list" @del="handelDel"></TodoMain>
    <TodoFooter :list="list" @clear="clear"></TodoFooter>
  </section>
</template>

<script>
import TodoHeader from './components/TodoHeader.vue'
import TodoMain from './components/TodoMain.vue'
import TodoFooter from './components/TodoFooter.vue'
export default {
  data() {
    return {
      list: JSON.parse(localStorage.getItem('list')) || [
        { id: 1, name: '打篮球' },
        { id: 2, name: '看电影' },
        { id: 3, name: '逛街' },
      ],
    }
  },
  components: {
    TodoHeader,
    TodoMain,
    TodoFooter,
  },
  watch: {
    list: {
      deep: true,
      handler(newVal) {
        localStorage.setItem('list', JSON.stringify(newVal))
      },
    },
  },
  methods: {
    handleAdd(todoName) {
      // console.log(todoName)
      this.list.unshift({
        id: +new Date(),
        name: todoName,
      })
    },
    handelDel(id) {
      // console.log(id);
      this.list = this.list.filter((item) => item.id !== id)
    },
    clear() {
      this.list = []
    },
  },
}
</script>

<style>
</style>
index.css
html,
body {
  margin: 0;
  padding: 0;
}
body {
  background: #fff;
}
button {
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  font-size: 100%;
  vertical-align: baseline;
  font-family: inherit;
  font-weight: inherit;
  color: inherit;
  -webkit-appearance: none;
  appearance: none;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

body {
  font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
  line-height: 1.4em;
  background: #f5f5f5;
  color: #4d4d4d;
  min-width: 230px;
  max-width: 550px;
  margin: 0 auto;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  font-weight: 300;
}

:focus {
  outline: 0;
}

.hidden {
  display: none;
}

#app {
  background: #fff;
  margin: 180px 0 40px 0;
  padding: 15px;
  position: relative;
  box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
#app .header input {
  border: 2px solid rgba(175, 47, 47, 0.8);
  border-radius: 10px;
}
#app .add {
  position: absolute;
  right: 15px;
  top: 15px;
  height: 68px;
  width: 140px;
  text-align: center;
  background-color: rgba(175, 47, 47, 0.8);
  color: #fff;
  cursor: pointer;
  font-size: 18px;
  border-radius: 0 10px 10px 0;
}

#app input::-webkit-input-placeholder {
  font-style: italic;
  font-weight: 300;
  color: #e6e6e6;
}

#app input::-moz-placeholder {
  font-style: italic;
  font-weight: 300;
  color: #e6e6e6;
}

#app input::input-placeholder {
  font-style: italic;
  font-weight: 300;
  color: gray;
}

#app h1 {
  position: absolute;
  top: -120px;
  width: 100%;
  left: 50%;
  transform: translateX(-50%);
  font-size: 60px;
  font-weight: 100;
  text-align: center;
  color: rgba(175, 47, 47, 0.8);
  -webkit-text-rendering: optimizeLegibility;
  -moz-text-rendering: optimizeLegibility;
  text-rendering: optimizeLegibility;
}

.new-todo,
.edit {
  position: relative;
  margin: 0;
  width: 100%;
  font-size: 24px;
  font-family: inherit;
  font-weight: inherit;
  line-height: 1.4em;
  border: 0;
  color: inherit;
  padding: 6px;
  box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
  box-sizing: border-box;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

.new-todo {
  padding: 16px;
  border: none;
  background: rgba(0, 0, 0, 0.003);
  box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03);
}

.main {
  position: relative;
  z-index: 2;
}

.todo-list {
  margin: 0;
  padding: 0;
  list-style: none;
  overflow: hidden;
}

.todo-list li {
  position: relative;
  font-size: 24px;
  height: 60px;
  box-sizing: border-box;
  border-bottom: 1px solid #e6e6e6;
}

.todo-list li:last-child {
  border-bottom: none;
}

.todo-list .view .index {
  position: absolute;
  color: gray;
  left: 10px;
  top: 20px;
  font-size: 22px;
}

.todo-list li .toggle {
  text-align: center;
  width: 40px;
  -webkit-appearance: none;
  appearance: none;
}

.todo-list li .toggle {
  opacity: 0;
}

.todo-list li .toggle + label {
  
  background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E');
  background-repeat: no-repeat;
  background-position: center left;
}

.todo-list li .toggle:checked + label {
  background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E');
}

.todo-list li label {
  word-break: break-all;
  padding: 15px 15px 15px 60px;
  display: block;
  line-height: 1.2;
  transition: color 0.4s;
}

.todo-list li.completed label {
  color: #d9d9d9;
  text-decoration: line-through;
}

.todo-list li .destroy {
  display: none;
  position: absolute;
  top: 0;
  right: 10px;
  bottom: 0;
  width: 40px;
  height: 40px;
  margin: auto 0;
  font-size: 30px;
  color: #cc9a9a;
  margin-bottom: 11px;
  transition: color 0.2s ease-out;
}

.todo-list li .destroy:hover {
  color: #af5b5e;
}

.todo-list li .destroy:after {
  content: '×';
}

.todo-list li:hover .destroy {
  display: block;
}

.todo-list li .edit {
  display: none;
}

.todo-list li.editing:last-child {
  margin-bottom: -1px;
}

.footer {
  color: #777;
  padding: 10px 15px;
  height: 20px;
  text-align: center;
  border-top: 1px solid #e6e6e6;
}

.footer:before {
  content: '';
  position: absolute;
  right: 0;
  bottom: 0;
  left: 0;
  height: 50px;
  overflow: hidden;
  box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 0 8px 0 -3px #f6f6f6,
    0 9px 1px -3px rgba(0, 0, 0, 0.2), 0 16px 0 -6px #f6f6f6,
    0 17px 2px -6px rgba(0, 0, 0, 0.2);
}

.todo-count {
  float: left;
  text-align: left;
}

.todo-count strong {
  font-weight: 300;
}

.filters {
  margin: 0;
  padding: 0;
  list-style: none;
  position: absolute;
  right: 0;
  left: 0;
}

.filters li {
  display: inline;
}

.filters li a {
  color: inherit;
  margin: 3px;
  padding: 3px 7px;
  text-decoration: none;
  border: 1px solid transparent;
  border-radius: 3px;
}

.filters li a:hover {
  border-color: rgba(175, 47, 47, 0.1);
}

.filters li a.selected {
  border-color: rgba(175, 47, 47, 0.2);
}

.clear-completed,
html .clear-completed:active {
  float: right;
  position: relative;
  line-height: 20px;
  text-decoration: none;
  cursor: pointer;
}

.clear-completed:hover {
  text-decoration: underline;
}

.info {
  margin: 50px auto 0;
  color: #bfbfbf;
  font-size: 15px;
  text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
  text-align: center;
}

.info p {
  line-height: 1;
}

.info a {
  color: inherit;
  text-decoration: none;
  font-weight: 400;
}

.info a:hover {
  text-decoration: underline;
}
@media screen and (-webkit-min-device-pixel-ratio: 0) {
  .toggle-all,
  .todo-list li .toggle {
    background: none;
  }

  .todo-list li .toggle {
    height: 40px;
  }
}

@media (max-width: 430px) {
  .footer {
    height: 50px;
  }

  .filters {
    bottom: 10px;
  }
}


main.js
webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'
import router from './router'
import './components/index.css'
Vue.config.productionTip = false
new Vue({
  el: '#app',
  router,
  components: { App },
  template: '<App/>'
})

效果图如下:
image.png

非父子通信(拓展)

event bus事件总线

作用:非父子组件之间,进行简易消息传递。(复杂场景→ Vuex)

步骤:

  1. 创建一个都能访问的事件总线 (空Vue实例)-utils/EventBus.js

    import Vue from 'vue'
    const Bus = new Vue()
    export default Bus
    
  2. A组件(接受方),监听Bus实例的事件

    created () {
      Bus.$on('sendMsg', (msg) => {
        this.msg = msg
      })
    }
    
  3. B组件(发送方),触发Bus的$emit事件

    Bus.$emit('sendMsg', '这是一个消息')
    
1682328229273.png

BaseA.vue(接受方)

<template>
  <div class="base-a">
    我是A组件(接受方)
    <p>{{msg}}</p>  
  </div>
</template>

<script>
import Bus from '../utils/EventBus'
export default {
  data() {
    return {
      msg: '',
    }
  },
  created() {
    Bus.$on('sendMsg', (msg) => {
      // console.log(msg)
      this.msg = msg
    })
  },
}
</script>

<style scoped>
.base-a {
  width: 200px;
  height: 200px;
  border: 3px solid #000;
  border-radius: 3px;
  margin: 10px;
}
</style>

BaseB.vue(发送方)

<template>
  <div class="base-b">
    <div>我是B组件(发布方)</div>
    <button @click="sendMsgFn">发送消息</button>
  </div>
</template>

<script>
import Bus from '../utils/EventBus'
export default {
  methods: {
    sendMsgFn() {
      Bus.$emit('sendMsg', '今天天气不错,适合旅游')
    },
  },
}
</script>

<style scoped>
.base-b {
  width: 200px;
  height: 200px;
  border: 3px solid #000;
  border-radius: 3px;
  margin: 10px;
}
</style>

BaseC.vue中代码

<template>
  <div class="base-c">
    我是C组件(接受方)
    <p>{{msg}}</p>  
  </div>
</template>

<script>
import Bus from '../utils/EventBus'
export default {
  data() {
    return {
      msg: '',
    }
  },
  created() {
    Bus.$on('sendMsg', (msg) => {
      // console.log(msg)
      this.msg = msg
    })
  },
}
</script>

<style scoped>
.base-c {
  width: 200px;
  height: 200px;
  border: 3px solid #000;
  border-radius: 3px;
  margin: 10px;
}
</style>

App.vue

<template>
  <div class="app">
    <BaseA></BaseA>
    <BaseB></BaseB>
    <BaseC></BaseC>
  </div>
</template>

<script>
import BaseA from './components/BaseA.vue'
import BaseB from './components/BaseB.vue'
import BaseC from './components/BaseC.vue'
export default {
  components:{
    BaseA,
    BaseB,
    BaseC
  }
}
</script>

<style>

</style>

发布通知并不是一对一的关系,任何监听了这个消息的组件都能收到这个消息

provide-inject(拓展)

作用:跨层级共享数据

  1. 父子间 provide 提供数据

export default {
  provide () {
    return {
       // 普通类型【非响应式】
       color: this.color, 
       // 复杂类型【响应式】
       userInfo: this.userInfo, 
    }
  }
}
  1. 子/孙组件inject 取值使用
export default {
  inject: ['color','userInfo'],
  created () {
    console.log(this.color, this.userInfo)
  }
}
1682329505511.png

示例:

SonA.vue

<template>
  <div class="SonA">我是SonA组件
    <GrandSon></GrandSon>
  </div>
</template>

<script>
import GrandSon from '../components/GrandSon.vue'
export default {
  components:{
    GrandSon
  }
}
</script>

<style>
.SonA {
  border: 3px solid #000;
  border-radius: 6px;
  margin: 10px;
  height: 200px;
}
</style>

SonB.vue

<template>
  <div class="SonB">
    我是SonB组件
  </div>
</template>

<script>
export default {

}
</script>

<style>
.SonB {
  border: 3px solid #000;
  border-radius: 6px;
  margin: 10px;
  height: 200px;
}
</style>

GrandSon.vue

<template>
  <div class="grandSon">
    我是GrandSon
    {{ color }} -{{ userInfo.name }} -{{ userInfo.age }}
  </div>
</template>

<script>
export default {
  inject: ['color', 'userInfo'],
}
</script>

<style>
.grandSon {
  border: 3px solid #000;
  border-radius: 6px;
  margin: 10px;
  height: 100px;
}
</style>

App.vue

<template>
  <div class="app">
    我是APP组件
    <button @click="change">修改数据</button>
    <SonA></SonA>
    <SonB></SonB>
  </div>
</template>

<script>
import SonA from './components/SonA.vue'
import SonB from './components/SonB.vue'
export default {
  provide() {
    return {
      // 简单类型 是非响应式的
      color: this.color,
      // 复杂类型 是响应式的
      userInfo: this.userInfo,
    }
  },
  data() {
    return {
      color: 'pink',
      userInfo: {
        name: 'zs',
        age: 18,
      },
    }
  },
  methods: {
    change() {
      this.color = 'red'
      this.userInfo.name = 'ls'
    },
  },
  components: {
    SonA,
    SonB,
  },
}
</script>

<style>
.app {
  border: 3px solid #000;
  border-radius: 6px;
  margin: 10px;
}
</style>

效果图如下:

image.png

v-model详解

v-model 原理

原理:v-model本质上是一个 语法糖 。例如应用在输入框上,就是value属性 和 input事件 的合写。

v-model = :value + input

作用:提供数据的双向绑定

(1)数据变,试图跟着变:value

(2)视图变,数据跟着变 @input

注意:$event 用于在模板中,获取事件的形参

<template>
  <div id="app" >
    <input v-model="msg" type="text">

    <input :value="msg" @input="msg = $event.target.value" type="text">
  </div>
</template>

image.png

event.target 触发事件源, .value拿到输入框的值,把拿到的值赋值给msg

示例:

App.vue

<template>
  <div>
    <input v-model="msg1" type="text"><br><br>
    //模板中获取事件的形参-$event获取
    <input :value="msg2" @input="msg2=$event.target.value" type="text"><br><br>
  </div>
</template>

<script>
export default {
   data(){
    return{
      msg1:'',
      msg2:''
    }
   }
}
</script>

<style>

</style>

模板中获取事件的形参-$event获取

效果如图:

image.png

表单类组件封装 & v-model简化代码

表单类组件封装
  1. 父传子:数据应该是父组件 props传递过来的,v-model 拆解 绑定数据
  2. 子传父:监听输入,子传父传值给父组件修改

不可直接使用 v-model,v-model为双向绑定,会改变绑定的值,所以需要v-model拆解

父组件(使用)

<BaseSelect:cityId="selectId"@事件名="selectId = $event"/>

image.png

image.png

示例:

BaseSelect.vue

<template>
  <div>
    <select :value="selectId" @change="selectCity">
      <option value="101">北京</option>
      <option value="102">上海</option>
      <option value="103">武汉</option>
      <option value="104">广州</option>
      <option value="105">深圳</option>
    </select>
  </div>
</template>

<script>
export default {
  props: {
    selectId: String,
  },
  methods: {
    selectCity(e) {
      this.$emit('changeCity', e.target.value)
    },
  },
}
</script>

<style>
</style>

App.vue

<template>
  <div class="app">
    <BaseSelect
      :selectId="selectId"
      @changeCity="selectId = $event"
    ></BaseSelect>
  </div>
</template>

<script>
import BaseSelect from './components/BaseSelect.vue'
export default {
  data() {
    return {
      selectId: '102',
    }
  },
  components: {
    BaseSelect,
  },
}
</script>

<style>

</style>

下拉菜单: @change

效果图如下:

image.png

v-model 简化代码

父组件 v-model简化代码,实现 子组件和 父组件的 双向绑定

  1. 子组件中:props 通过value接收,事件触发通过 input
  2. 父组件中:v-model 给组件直接绑数据(:value+ @input)

image.png

子组件代码基本不变

  • 只需将props 改为 value
  • 触发事件通过input

image.png

BaseSelect.vue

<template>
  <div>
    <select :value="value" @change="handleeChange">
      <option value="101">北京</option>
      <option value="102">上海</option>
      <option value="103">武汉</option>
      <option value="104">广州</option>
      <option value="105">深圳</option>
    </select>
  </div>
</template>

<script>
export default {
  props:{
    value:String
  },
  methods:{
    handleeChange(e){
       //console.log(e.target.value)
       this.$emit('input',e.target.value)
    }
  }
}
</script>

<style>
</style>

App.vue

<template>
  <div class="app">
    <BaseSelect v-model="selectId">
   //更改为v-model
  </BaseSelect>
  </div>
</template>

<script>
import BaseSelect from './components/BaseSelect.vue'
export default {
  data() {
    return {
      selectId: '104'
    }
  },
  components: {
    BaseSelect,
  },
}
</script>

<style>
</style>

透传Attributes

透传 attribute 指的是传递给一个组件,却没有被该组件声明为props 或emits 的attribute 或者v-on事件监听器。最常见例子 class styleid

  • 必须是唯一根元素
  • 禁止继承inheritAttrs:false

.sync 修饰符

  1. 作用:可以实现 子组件父组件数据双向绑定,简化代码

  2. 特点:props属性名可以自定义,非固定为value

简单理解:子组件可以修改父组件传过来的props值

  1. 场景

封装弹框类的基础组件, visible属性 true显示 false隐藏

  1. 本质

.sync修饰符 就是 :属性名@update:属性名 合写

5.语法

父组件代码

//.sync写法
<BaseDialog :visible.sync="isShow" />
--------------------------------------
//完整写法
<BaseDialog 
  :visible="isShow" 
  @update:visible="isShow = $event" 
/>

子组件代码

props: {
  visible: Boolean
},

this.$emit('update:visible', false)

示例:

App.vue

<template>
  <div class="app">
    <button @click="openDialog">退出按钮</button>
    <!-- isShow.sync  => :isShow="isShow" @update:isShow="isShow=$event" -->
    <BaseDialog :isShow.sync="isShow"></BaseDialog>
  </div>
</template>

<script>
import BaseDialog from './components/BaseDialog.vue'
export default {
  data() {
    return {
      isShow: false,
    }
  },
  methods: {
    openDialog() {
      this.isShow = true
      // console.log(document.querySelectorAll('.box')); 
    },
  },
  components: {
    BaseDialog,
  },
}
</script>

<style>
</style>

BaseDialog.vue

<template>
  <div class="base-dialog-wrap" v-show="isShow">
    <div class="base-dialog">
      <div class="title">
        <h3>温馨提示:</h3>
        <button class="close" @click="closeDialog">x</button>
      </div>
      <div class="content">
        <p>你确认要退出本系统么?</p>
      </div>
      <div class="footer">
        <button>确认</button>
        <button>取消</button>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  props: {
    isShow: Boolean,
  },
  methods:{
    closeDialog(){
      this.$emit('update:isShow',false)
    }
  }
}
</script>

<style scoped>
.base-dialog-wrap {
  width: 300px;
  height: 200px;
  box-shadow: 2px 2px 2px 2px #ccc;
  position: fixed;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  padding: 0 10px;
}
.base-dialog .title {
  display: flex;
  justify-content: space-between;
  align-items: center;
  border-bottom: 2px solid #000;
}
.base-dialog .content {
  margin-top: 38px;
}
.base-dialog .title .close {
  width: 20px;
  height: 20px;
  cursor: pointer;
  line-height: 10px;
}
.footer {
  display: flex;
  justify-content: flex-end;
  margin-top: 26px;
}
.footer button {
  width: 80px;
  height: 40px;
}
.footer button:nth-child(1) {
  margin-right: 10px;
  cursor: pointer;
}
</style>

:visible.sync => :visible + @update:visible

效果图如下:

image.png

ref 和 $ref获取dom和组件

ref 和 $ref

作用:利用ref 和 $ref可以获取dom元素或组件实例

特点:查找范围 —>当前组件内(更精确稳定)

querySelector 查找范围:整个页面

获取dom
  1. 目标标签-添加ref属性

<div ref="chartRef">我是渲染图表的容器</div>

  1. 恰当时机,通过this.$refs.xxx,获取目标标签(恰当时机:当前dom需要存在,mounted之后)
mounted(){
  console.log(this.$refs.chartRef)
},

示例:

BaseChart.vue

<template>
  <div class="base-chart-box" ref="baseChartBox">子组件</div>
</template>

<script>
import * as echarts from 'echarts'

export default {
  mounted() {
    // 基于准备好的dom,初始化echarts实例
    // document.querySelector 会查找项目中所有的元素
    // $refs只会在当前组件查找盒子
    // var myChart = echarts.init(document.querySelector('.base-chart-box'))
    var myChart = echarts.init(this.$refs.baseChartBox)
    // 绘制图表
    myChart.setOption({
      title: {
        text: 'ECharts 入门示例',
      },
      tooltip: {},
      xAxis: {
        data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子'],
      },
      yAxis: {},
      series: [
        {
          name: '销量',
          type: 'bar',
          data: [5, 20, 36, 10, 10, 20],
        },
      ],
    })
  },
}
</script>

<style scoped>
.base-chart-box {
  width: 400px;
  height: 300px;
  border: 3px solid #000;
  border-radius: 6px;
}
</style>

App.vue

<template>
  <div class="app">
    <div class="base-chart-box">
      这是一个捣乱的盒子
    </div>
    <BaseChart></BaseChart>
  </div>
</template>

<script>
import BaseChart from './components/BaseChart.vue'
export default {
  components:{
    BaseChart
  }
}
</script>

<style>
.base-chart-box {
  width: 300px;
  height: 200px;
}
</style>
获取组件
  1. 目标组件-添加ref属性

<BaseForm ref="baseForm"></BaseForm>

  1. 恰当时机,通过 this.$refs.xxx,获取目标组件,就可以调用组件对象里面的方法 this.$refs.baseForm.组件方法()

示例: BaseForm.vue

<template>
  <div class="app">
    <div>
      账号: <input v-model="username" type="text">
    </div>
     <div>
      密码: <input v-model="password" type="text">
    </div>
    <div>
      <button @click="getFormData">获取数据</button>
      <button @click="resetFormData">重置数据</button>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      username: 'admin',
      password: '123456',
    }
  },
  methods: {
    //方法一 getValues 获取表单数据,返回一个对象
    getFormData() {
      console.log('获取表单数据', this.username, this.password);
    },
    //方法二 重置表格
    resetFormData() {
      this.username = ''
      this.password = ''
      console.log('重置表单数据成功');
    },
  }
}
</script>

<style scoped>
.app {
  border: 2px solid #ccc;
  padding: 10px;
}
.app div{
  margin: 10px 0;
}
.app div button{
  margin-right: 8px;
}
</style>

App.vue

<template>
  <div class="app">
    <h4>父组件 -- <button>获取组件实例</button></h4>
    <BaseForm></BaseForm>
  </div>
</template>

<script>
import BaseForm from './components/BaseForm.vue'
export default {
  data(){
    return{

    }
  },
  methods: {
    handleGet(){
      console.log(this.$refs.baseForm.getFormData())
    },
    handleReset(){ 
      this.$refs.baseForm. resetFormData()
    }
  },
  components:{
    BaseForm
  }
}
</script>

<style>
</style>

方法一: getValues 获取表单数据,返回一个对象

方法二: resetValues 重置表单

Vue异步更新和$nextTick

异步更新

需求:编辑标题,编辑框自动聚焦

  1. 点击编辑,显示编辑框
  2. 让编辑框获取焦点
this.isShowEdit = true 
//显示输入框
this.$refs.inp.focus() 
//获取焦点

$nextTick

$nextTick:等DOM更新后,才会触发执行此方法里的函数体

语法:this.$nextTick(函数体)

this.$nextTick(()=>{
  this.$refs.inp.focus()
})

示例:

App.vue

<template>
  <div class="app">
    <div v-if="isShowEdit">
      <input v-model="editValue" type="text"  ref="inp" />
      <button>确认</button>
    </div>
    <div v-else>
      <span>{{ title }}</span>
      <button @click="handleEdit">编辑</button>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      title: '大标题',
      isShowEdit: false,
      editValue: '',
    }
  },
  methods: {
   handleEdit(){
    //显示输入框(异步dom更新)
    this.isShowEdit = true
    //让输入框获取焦点
    this.$nextTick(()=>{
      console.log(this.$refs.inp)
      this.$refs.inp.focus()
    })
    // setTimeout(() => {
    //   this.$refs.inp.focus()
    // },1000)
    
  }
}
}
</script>

<style>
</style>

效果图如下:

image.png

点击编辑时:

image.png
  • $nextTick等dom更新完,立刻去执行准备的函数体

  • setTimeout存在等待时间