Vue学习第四天

269 阅读6分钟

组件的三大组成部分

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

scoped样式冲突

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

  1. 全局样式:默认组件中的样式都会作用到全局
  2. 局部样式:可以给组件加上scoped属性,让样式只作用于当前组件

应用

  • 这是原来APP.vue,其中有两个组件baseone和basetwo
  • 组件分为三部分
    • 导入
    • 注册
    • 使用
<template>
  <div id="app">
    <!-- <img alt="Vue logo" src="./assets/logo.png">
    <HelloWorld msg="Welcome to Your Vue.js App"/> -->
    //使用
    <BaseOne></BaseOne>
    <BaseTwo></BaseTwo>

  </div>
</template>

<script>
//导入
//import HelloWorld from './components/HelloWorld.vue'
import BaseOne from './components/BaseOne'
import BaseTwo from './components/BaseTwo'

export default {
  name: 'App',
  //注册
  components: {
    BaseOne,
    BaseTwo
  }
}
</script>

它们的.vue文件为下面这样,此时给他们的.vue文件即使分别加样式,也会同时改变。

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

<script>
export default {

}
</script>
<style>
 
</style>

此时给style加上scoped,<style scoped>,就会变成局部组件,都会有独立的样式

scoped原理:

  1. 给当前组件模板的所有元素,都会添加上一个自定义属性,叫 data-v-hash值,比如data-v-5f6a9d56
  • 同一组件的hash值一般相同
  • 不同组件hash值自然不同,所以可以用这个来区分不同的组件
  1. css选择器后面,被自动处理,添加上了不同选择器 div[data-v-5f6a9d56]

data必须是一个函数

一个组件的data选项必须是一个函数——>保证每个组件实例,维护独立的一份数据对象

  • 每次创建新的组件实例,都会新执行一次data函数,得到一个新对象
//在export default{}里写
  data: function () {
    return {
      count: 100,
    }
  }

整体就是:​

<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 scoped>
 .base-count {
  margin: 20px;
}
</style>
  1. 每次创建新的组件实例,都会新执行一次data函数,得到一个新对象
  2. 保证每个组件实例,维护独立的一份数据对象,而这两句到底是什么意思呢?
  • 意思就是,如果你现在在app.vue里面写了多个这个组件,它们也分别都是独立的,你动上一个并不会影响它其他几个
  • data用几次,执行几次

组件通信

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

  • 组件的数据是独立的,无法直接访问其他组件的数据
  • 想用其他组件的数据——>组件通信

组件关系分类:

  • 父子关系
  • 非父子关系

组件通信解决方案

父子关系

父 ——> 子

  1. 如果子组件中需要用到父组件的数据,那么就需要 父组件通过props将数据传递给子组件
  2. 子组件利用$emit,通知父组件修改更新

现在有一个需求:

  • 就是现在想把父组件‘学前端,来黑马!’这个title用在子组件中,但是目前这样理论上是实现不了的

正确的流程应该如下图:

  • 先给父组件中的子组件添加自定义属性(动态传递需要带冒号),并传值,如下图的mytitle为值
  • 然后在子组件的vue中通过props接受你这个冒号的内容
  • 最后直接在子组件上面模板中使用 ​

子 ——> 父

现在有一个需求:

  • 就是想通过点击按钮来修改上面的title
 <div class="son" style="border: 3px solid #000; margin: 10px">
    我是Son组件 {{ title }}
    <button @click="changeFn">修改title</button>

正确的流程为:

  1. 先在子组件的template里写好你的按钮,并给它注册了一个点击事件,并在下面写了一个methods方法。但是你此时在methods里直接写是没有用的
<button @click="changeFn">修改title</button>
  1. 通过this.$emit() 向父组件发送通知
methods: {
    changeFn() {
      // 通过this.$emit() 向父组件发送通知
      this.$emit('changTitle','传智教育')
    },
  },
  1. 父组件为了收到消息,需要在子组件上绑上监听
 <Son :title="myTitle" @changTitle="handleChange"></Son>
  1. 在父组件中提供对应的处理函数,提供逻辑

整体流程图为: ​

props详解

定义、作用、特点、应用

props定义: 组件上注册的一些自定义属性props:[]

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

props特点:

  • 可以传递 任意数量 的props
  • 可以传递 任意类型 的props

父组件中提供数据,给这个子组件以添加属性的方式传值

tip:兴趣爱好那里,通过join转换为字符串,使用顿号分割

props校验

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

  • 分为:
    • 类型校验
    • 非空校验
    • 默认值
    • 自定义

类型校验

可以严格约束你传过来的到底是什么类型

prop:{校验的属性名:类型},类型可以是number、string、boolean、array、object、function...

//原来
props:['w']
//现在把它写成对象的方式
//直接key的地方写原来的属性
props: {
    w: Number,
  },

更复杂的其他的校验

类型校验只需在校验的属性名后面写类型即可,但是,如果你有更高的需求,就需要在这里写成对象形式

props:{
  检验的属性名:{
    type:类型,//跟上面类型校验那一个意思
    required:true,//是否必填
    default:默认值,//给一个默认值
    //如果对传进来的值有更细节的要求,用下面这个自定义校验
    //return true则表示通过了这个校验
    //里面这个value形参是可以拿到你要校验的这个值的
    validator(value){
      //自定义校验的逻辑
      return 是否通过校验
    }
  }
}

比如:

props: {
    w: {
      type: Number,
      required: true,
      default: 0,
      validator(val) {
        // console.log(val)
        if (val >= 100 || val <= 0) {
          console.error('传入的范围必须是0-100之间')
          return false
        } else {
          return true
        }
      },
    },
  },
}

prop和data关系与区别、单向数据流

prop和data共同点: 都可以给组件提供数据

区别:

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

单向数据流: 当父组件的prop更新,接着向下流,也会影响到子组件,子组件变化,视图也就更新了

还是这个案例:

App.vue,父组件

<template>
  <div id="app">
    <BaseTwo :count="count" @changeCount="handleChange"></BaseTwo>
  </div>
</template>

<script>

import BaseTwo from './components/BaseTwo'

export default {
  name: 'App',
  components: {
    BaseTwo
  },
  data(){
    return {
      count:100
    }
  },
  methods:{
    handleChange(newVal){
      // console.log(newVal);
      this.count = newVal
    }
  }
}

</script>

<style>

</style>

BaseTwo.vue,子组件

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

<script>
export default {

  props: {
  //父传子
    count: {
      type: Number,
    },
  },
  methods: {
  //子传父
    handleSub() {
      this.$emit('changeCount', this.count - 1)
    },
    handleAdd() {
      this.$emit('changeCount', this.count + 1)
    },
  },
}
</script>

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

小黑记事本——组件通信案例

1. 拆分基础组件(三步走)

//1.
<template>
  <!-- 主体区域 -->
  <section id="app">
    <TodoHeader></TodoHeader>
    <TodoMain ></TodoMain>
    <TodoFooter></TodoFooter>
  </section>
</template>

<script>
//2.
import TodoHeader from './components/TodoHeader.vue'
import TodoMain from './components/TodoMain.vue'
import TodoFooter from './components/TodoFooter.vue'

//3.
 components: {
    TodoHeader,
    TodoMain,
    TodoFooter,
  }
  </script>

2.渲染待办任务

    1. 提供数据——> 提供在 公共(data) 的父组件App.vue
      • 这样的话,如果之后子组件要用,直接父传子就可以

为什么提供在父组件里?(上面"data必须是一个函数"

  1. 每次创建新的组件实例,都会新执行一次data函数,得到一个新对象
  2. 保证每个组件实例,维护独立的一份数据对象,而这两句到底是什么意思呢?
  • 意思就是,如果你现在在app.vue里面写了多个这个组件,它们也分别都是独立的,你动上一个并不会影响它其他几个
  • data用几次,执行几次
//在App.vue,父组件中提供数据
data() {
    return {
      //提供数据
      list: [
        { id: 1, name: '看琉璃' },
        { id: 2, name: '逛淘宝' },
        { id: 3, name: '打游戏' },
      ],
    }
  },
    1. 通过父传子,将数据传递给TodoMain这几个组件
//App.vue中
    <TodoMain :list="list"></TodoMain>
//TodoMain中    
     props: {
    list: {
      type: Array,
    },
  },
    1. 利用v-for渲染
<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>

3. 添加任务

    1. 拿到输入框数据(也就是收集表单数据) ——> v-model——>data里的todoName
    1. 监听事件(回车+点击)@keyup.enter@click——>methods
    1. 子传父,将任务名称传递给父组件
    1. 父组件进行unshift添加
   <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(){
      if(this.todoName.trim()===''){
        alert('任务名名称不能为空')
        return
      }
      // console.log(this.todoName)
      //子传父
      this.$emit('add',this.todoName)
      this.todoName = ''
    }
  }
}
</script>

传给父:

    <TodoHeader @add="handleAdd"></TodoHeader>
    
     methods: {
       handleAdd(todoName) {
        // console.log(todoName)
         this.list.unshift({
         id: +new Date(),
         name: todoName,
      })
    },
    }

4. 删除功能

    1. 监听删除的点击
    1. 因为最开始我们的数据就在父组件,所以第二步还是子传父,将删除的id传递给父组件,记得带id
    1. 在父组件处进行删除fliter
//在父组件中
    <TodoMain :list="list" @del="handelDel"></TodoMain>
    
 handelDel(id) {
      // console.log(id);
      this.list = this.list.filter((item) => item.id !== id)
    },
//TodoMain中
      <button class="destroy" @click="handleDel(item.id)"></button>
      
 methods: {
    handleDel(id) {
      this.$emit('del', id)
    },
  },

5.底部合计和清空

底部合计: 父传子(把数据传给子)+渲染

底部清空: 子传父,通知到父组件,由父组件进行操作清空

//父组件
    <TodoFooter :list="list" @clear="clear"></TodoFooter>
     clear() {
      this.list = []
    },
//子组件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>

6. 持久化存储:用watch去深度监听list的变化——>变化了就往本地存储——>进入页面优先读取本地

//父组件中(是与methods并列的)

 list: JSON.parse(localStorage.getItem('list')) ||[
 ...
 ]


watch: {
    list: {
      deep: true,//深度监视
      //处理函数
      handler(newVal) {
        localStorage.setItem('list', JSON.stringify(newVal))
      },
    },
  },

非父子通信(拓展)——event bus事件总线

  • 非父子组件之间,进行简易的消息传递
  • 并不是一对一的,而是可以一对多的
  • 只要监听了就都可以接收到然后进行渲染

思路:

在src文件夹中新建一个文件夹utils——>然后再新建一个EventBus.js文件

  1. 创建一个都能访问到的事件总线Event Bus(因为A、B组件本身没什么关联,所以我们现在要创建关联)
//创建一个vue实例,并进行导出
import Vue from 'vue'
//创建实例new vue
const Bus = new Vue()
export default Bus
  1. A组件(接受方),监听Bus实例的事件
 <p>{{msg}}</p>  
 
 
//再A里导入一下
import Bus from '../utils/EventBus'
//再导出,进行监听,监听Bus
export default{
created(){
//第一个‘’里写的是你要监听的事件名,后面是监听到后要干嘛
  Bus.$on('sendMsg',(msg)=>{
    this.msg=msg
  })
}
}
  1. B组件(发送方),触发Bus实例的事件,Bus.$emit('sendMsg', '这是一条消息')
  <button @click="sendMsgFn">发送消息</button>

import Bus from '../utils/EventBus'
export default {
  methods: {
    sendMsgFn() {
      Bus.$emit('sendMsg', '这是一条消息')
    },
  },
}

非父子通信(拓展)——provide & inject

provide & inject作用:跨层级 共享数据(爷孙)

  • 爷爷provide的数据可以直接inject给孙子
  • 不需要爷传父,父传子
  1. 父组件provide
export default {
  provide() {
    return {
      // 简单类型 是非响应式的
      color: this.color,
      // 复杂类型 是响应式的
      userInfo: this.userInfo,
    }
  },
  
 //———————————————————————— 
   data() {
    return {
      color: 'pink',//简单类型,不响应,不推荐
      //复杂类型,响应,推荐
      userInfo: {
        name: 'zs',
        age: 18,
      },
    }
  },
  1. 子/孙组件inject直接接收使用,然后再渲染
export default {
  inject: ['color', 'userInfo'],
}

v-model详解

原理

原理: v-model本质上是一个语法糖。

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

  • 数据变,视图跟着变:value
  • 视图变,数据跟着变@input

例如应用在输入框上,就是value属性和input事件的合写

tip:$event用在模板中,获取事件的形参 ​

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

表单类组件封装

  1. 父传子:value,数据应该是父组件props传递过来的,v-model拆解绑定数据(因为你不拆的话,父传子理论上是无法修改的)
  2. 子传父@input,监听输入,子传父传值给父组件修改
<template>
  <div class="app">
    <BaseSelect
      :cityId="selectId"
      @change="selectId=$event" 
    ></BaseSelect>
  </div>
</template>

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

<style>
</style>
<template>
  <div>
    <select :value="cityId" @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: {
    cityId: String,
  },
  methods: {
    selectCity(e) {
      this.$emit('changeId', e.target.value)
    },
  },
}
</script>

<style>
</style>

v-model简化代码

  1. 子组件中:props通过value接收,事件触发input(这个不能简写成v-model,必须拆解)
  2. 父组件中:v-model给组件直接绑数据(把两个合并成v-model了)
1. 
       v-model="selectId"
       <!-- 简化 -->
       <!-- :cityId="selectId"  @change="selectId=$event" -->
      
2. 
  props: {
    value: String,
  },
  //props: {
  //  cityId: String,
  //},
   
 3. 
 <select :value="value" @change="selectCity">
    //<select :value="cityId" @change="selectCity">
    
 4.
 methods: {
    selectCity(e) {
      this.$emit('input', e.target.value)
    },
  },
}
// methods: {
//    selectCity(e) {
//      this.$emit('changeId', e.target.value)
//    },
//  },
//}
 

sync修饰符

跟v-model作用一样,但是语法略有不同

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

特点prop属性名,可以自定义,非固定为value

isShow.sync => :isShow="isShow" @update:isShow="isShow=$event"

实例:“确认要退出吗”

父组件中

//直接简写成
    <BaseDialog :isShow.sync="isShow"></BaseDialog>
    
    
     data() {
    return {
      isShow: false,
    }
  },

完整为:

<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
    },
  },
  components: {
    BaseDialog,
  },
}
</script>

<style>
</style>

子组件中

  <div class="base-dialog-wrap" v-show="isShow">

export default {
  props: {
    isShow: Boolean,
  },
  methods:{
    closeDialog(){
    //update:...为固定事件名
      this.$emit('update:isShow',false)
    }
  }
}

完整为:

<template>
  <!-- 显示隐藏  v-show="isShow"-->
  <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>

ref和$refs 获取dom和组件

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

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

querySelector查找范围——>整个页面

获取dom元素

  1. 获取dom:(在子组件)

目标标签——添加ref属性

 <div class="base-chart-box" ref="baseChartBox">子组件</div>
  1. 恰当时机,通过this.$refs.xxx,获取目标标签(在子组件)
mounted() {
    // 基于准备好的dom,初始化echarts实例
    // document.querySelector 会查找项目中所有的元素
    // $refs只会在当前组件查找盒子
    // var myChart = echarts.init(document.querySelector('.base-chart-box'))
var myChart = echarts.init(this.$refs.baseChartBox)
}

组件实例

  1. 获取组件

目标组件——添加ref属性

<BaseForm ref="baseForm"></BaseForm>
  1. 恰当时机,通过this.$refs.xxx,获取目标组件(在子组件),

就可以 调用组件对象里面的方法

this.$refs.baseForm.组件方法()

Vue异步更新——解决方法$nextTick

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

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

  • 语法:this.$nextTick(函数体)
  1. 点击编辑,显示编辑框
  2. 让编辑框立刻获取焦点

原本代码思路:

使用$nextTick解决

//区别!!!!!!!!!!!!!,把原来的放到这个nextTick里
     this.$nextTick(()=>{
        this.$refs.inp.focus()
      })
      //this.$refs.inp.focus()

tip:setTimeout虽然也可以解决异步更新问题,但是它等待的时间需要你自己设置,但是你也不知道它多久才能渲染好,所以不够精准

完整代码就是:

//在APP.vue父组件里!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
<template>
  <div class="app">
    <div v-if="isShowEdit">
      <input ref="inp" type="text" v-model="editValue"  />
      <button>确认</button>
    </div>
    <div v-else>
      <span>{{ title }}</span>
      <!-- 注册点击事件,一点的话isShowEdit变成true显示出来 -->
      <button @click="handleEdit">编辑</button>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      title: '大标题',
      //原来
      isShowEdit: false,
      editValue: '',
    }
  },
  methods: {
    handleEdit(){
      //1.显示输入框
      //(vue异步dom更新),还没被完全渲染出来
      this.isShowEdit=true
      //2.让输入框获取焦点
      this.$nextTick(()=>{
        this.$refs.inp.focus()
      })
      //this.$refs.inp.focus()
    }
   
  },
}
</script>

<style>
</style>