Vue2:组件高级(上)

简介: Vue2:组件高级(上)

Vue2:组件高级(上)

Date: May 20, 2023

Sum: 组件样式冲突、data函数、组件通信、props、组件生命周期、vue3.x中全局配置axios


目标:

能够掌握 watch 侦听器的基本使用


能够知道 vue 中常用的生命周期函数


能够知道如何实现组件之间的数据共享


能够知道如何在 vue3.x 的项目中全局配置 axios


前言:以下使用较老的axios,否则会报错

npm i axios@0.21.1 -S

组件之间的样式冲突

样式冲突问题:

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


导致组件之间样式冲突的根本原因是:


① 单页面应用程序中,所有组件的 DOM 结构,都是基于唯一的 index.html 页面进行呈现的


② 每个组件中的样式,都会影响整个 index.html 页面中的 DOM 元素


思考:如何解决组件样式冲突的问题


为每个组件分配唯一的自定义属性,在编写组件样式时,通过属性选择器来控制样式的作用域,示例代码如下:

c4fd05a0634a79b4ad0bc77514c8781f.png

style 节点的 scoped 属性

为了提高开发效率和开发体验,vue 为 style 节点提供了 scoped 属性,从而防止组件之间的样式冲突问题:

adc01c2277ddb9c640f3808c128e5d11.png

注意:父组件与子组件都要的style都要加上scoped


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


1.全局样式:默认组件中的样式会作用到全局

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


原理:


1.当前组件内标签都被添加data-v-hash值 的属性

2.css选择器都被添加 [data-v-hash值] 的属性选择器


最终效果: 必须是当前组件的元素, 才会有这个自定义属性, 才会被这个样式作用到

ff6c0e8aa96b39b08023498fb73ba405.png


/deep/ 样式穿透

如果给当前组件的 style 节点添加了 scoped 属性,则当前组件的样式对其子组件是不生效的。如果想让某些样式对子组件生效,可以使用 /deep/ 深度选择器。

5bd8a5b9b9a41cb4fdb4cc9204d63b9f.png

注意:/deep/ 是 vue2.x 中实现样式穿透的方案。在 vue3.x 中推荐使用 :deep() 替代 /deep/。


Vue3中的样式结构::deep(标签)


<style lang="less" scoped>
  p {
    color: red;
  }
  :deep(h3) {
    color: blue
  }
</style>

data必须是一个函数

原因:


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


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

497cb0d005222eeb7cd6f9f14a067839.png

举例:


data()函数能够保证每个组件的数据是独立的

ecc08a68f84005271429a799ab8f875b.png

组件通信

基础概念:

基础概念:


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


组件的数据是独立的,无法直接访问其他组件的数据。

想使用其他组件的数据,就需要组件通信


组件之间的关系:


在项目开发中,组件之间的关系分为如下 3 种:


① 父子关系 ② 兄弟关系 ③ 后代关系


AB是父子关系,BC有一个共同的父级节点,故二者为兄弟关系。B和EFI都为特殊的兄弟关系。


A和DGH属于后代关系

1a31b555f850c28613b647425c8af214.png

父子组件之间的数据共享

父子组件之间的数据共享又分为:


① 父 -> 子共享数据 ② 子 -> 父共享数据 ③ 父 <-> 子双向数据同步


通信过程:


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


2-子组件利用 $emit 通知父组件修改更新

19e421ec5c295cd4c19c1b330d8eca3f.png

父向子组件共享数据

父组件通过 v-bind 属性绑定向子组件共享数据。同时,子组件需要使用 props 接收数据。

786c84840ce2f3e6486cc0e080938397.png

案例:


App.vue

<template>
  <div>
    <h1>MyAPP -- {{ count }}</h1>
    <!-- 1.给最爱你标签,添加属性的方式,传值 -->
    <button @click="count += 1">父+1</button>
    <my-son :num="count"></my-son>
  </div>
</template>
<script>
import MySon from './Son.vue'
export default {
  name: 'MyApp',
  components: {
    MySon,
  },
  data() {
    return {
      count: 0,
    }
  }
}
</script>

MySon.vue

<template>
  <div>
    <!-- 3.渲染使用 -->
    <h2>MySon -- {{ num }}</h2>
  </div>
</template>
<script>
export default {
  name: 'MySon',
  // 2. 通过props进行接收
  props: ['num']
}
</script>

效果:

83aee7a7c86001546729243fac0da54c.png

子向父组件共享数据

子组件通过自定义事件的方式向父组件共享数据。


具体步骤:


子组件:


1.声明自定义事件 2. 数据变化时,触发自定义事件


父组件:


1.监听子组件的自定义事件 numchang 2.通过形参,接收子组件传递过来的数据

9bfabdb5d3ed22441a845816c486bf16.png


案例:


App.vue

<template>
  <div>
    <h1>MyAPP -- {{ count }}</h1>
    <button @click="count += 1">父+1</button>
    <!-- 1. 监听子组件的自定义事件 numchange -->
    <my-son :num="count" @numchange="getNum"></my-son>
  </div>
</template>
<script>
import MySon from './Son.vue'
export default {
  name: 'MyApp',
  components: {
    MySon,
  },
  data() {
    return {
      count: 0,
    }
  },
  methods: {
    getNum(num) { // 2. 通过形参,接收子组件传递过来的数据
      this.count = num
    }
  }
}
</script>

Son.vue

<template>
  <div>
    <h2>MySon -- {{ num }}</h2>
    <button @click="add">+1</button>
  </div>
</template>
<script>
export default {
  name: 'MySon',
  props: ['num'],
  emits: ['numchange'], //1. 声明自定义事件
  methods: {
    add() {
      this.$emit('numchange', this.num + 1) //2,数据变化时,触发自定义事件
    }
  }
}
</script>

效果:

6990031eda96e5e6d15b777b4658e7d0.png

父子组件之间数据的双向同步

父组件在使用子组件期间,可以使用 v-model 指令维护组件内外数据的双向同步:


具体步骤:


1.父组件向子组件的props中传递数据

  1.这里通过 v-model 方式进行双向数据绑定,维护组件两方数据同步

2.子组件声明emits属性,组件内的元素需要以 update: 的方式开头,这里需要更新哪个数据,就把相应数据的值丢过来,比如number

  1.通过 $emits 的方式将数据发送出去

b3acf66492e677f61f62ee6140ad07bc.png

好处:父组件中不用再监听自定义事件,也不用再额外定义事件处理函数


案例:


Code:


App.vue

<template>
  <div>
    <h1>MyAPP -- {{ count }}</h1>
    <button @click="count += 1">父+1</button>
    <my-son v-model:num="count" ></my-son>
  </div>
</template>
<script>
import MySon from './Son.vue'
export default {
  name: 'MyApp',
  components: {
    MySon,
  },
  data() {
    return {
      count: 0,
    }
  },
}
</script>

Son.vue

<template>
  <div>
    <h2>MySon -- {{ num }}</h2>
    <button @click="add">+1</button>
  </div>
</template>
<script>
export default {
  name: 'MySon',
  props: ['num'],
  emits: ['update:num'],
  methods: {
    add() {
      // this.$emit('numchange', this.num + 1)
      this.$emit('update:num', this.num + 1)
    }
  }
}
</script>

效果:

9044d83556ade2d8a231fce6717901a2.png


兄弟组件之间的数据共享

2023Vue教程的做法


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


步骤:


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


注:把这个放在utils下的EventBus.js中

import Vue from 'vue'
const Bus = new Vue()
export default Bus

2-A组件(接受方),监听Bus的 $on事件

// 先导入Bus
import Bus from '../utils/EventBus'
export default {
  data() {
    return {
      msg: '',
    }
  },
  // 再从 created 阶段就监听 $on 事件
  created() {
    Bus.$on('sendMsg', (msg) => {
      // console.log(msg)
      this.msg = msg
    })
  },
}

3-B组件(发送方),触发Bus的$emit事件


注:这个在组件内

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

图示:


注意:这是个一对多的发送

34b076a7260f8219d356df25ae2b6932.png

案例:传递A组件数据给B组件

44f44a29cf15ce0ca71ed90659efff7f.png

Code:


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>

2021Vue教程的做法:


兄弟组件之间实现数据共享的方案是 EventBus。


可以借助于第三方的包 mitt 来创建 eventBus 对象,从而实现兄弟组件之间的数据共享。


示意图如下:

ae790621acb16cee69335631bbf2d445.png

理解:在数据接收方调用on方法来声明自定义事件,在数据发送方通过emit方法来触发emit事件


3.1 安装 mitt 依赖包


在项目中运行如下的命令,安装 mitt 依赖包:

npm install mitt@2.1.0

3.2 创建公共的 EventBus 模块


在项目中创建公共的 eventBus 模块如下:

// eventBus.js
// 导入 mitt 包
import mitt from 'mitt'
// 创建 EventBus 的实例对象
const bus = mitt()
// 将 EventBus 的实例对象共享出去
export default bus

3.3 在数据接收方自定义事件


在数据接收方,调用 bus.on(‘事件名称’, 事件处理函数) 方法注册一个自定义事件。


示例代码如下:

// 导入 eventBus.js 模块, 得到共享的bus对象
export default {
  data() {return { count: 0}},
  created() {
    // 在created生命周期函数中声明自定义事件
    // 调用 bus.on 方法注册一个自定义事件,通过事件处理函数的形参数接收数据
    bus.on('countChange', (count) => {
      this.count = count
    })
  }
}

3.4 在数据接发送方触发事件


在数据发送方,调用 bus.emit(‘事件名称’, 要发送的数据) 方法触发自定义事件。示例代码如下:

// 导入 eventBus.js 模块,得到共享的 bus 对象
import bus from './eventBus.js'
export default {
  data() {return { count: 0}},
  methods: {
    addCount() {
      this.count++
      bus.emit('countChange', this.count) // 调用 bus.emit() 方法触发自定义事件,并发送数据
    } 
  }
}

案例:


Code:


Left.vue


<template>
  <div>
    <h2>Left--数据发送方--num的值为: {{ count }}</h2>
    <button @click="addCount">+1</button>
  </div>
</template>
<script>
import bus from './eventBus.js'
export default {
  name: 'MyLeft',
  data() {
    return {
      count: 0,
    }
  },
  methods: {
    addCount() {
      this.count++
      bus.emit('countChange', this.count)
    }
  }
}
</script>

Right.vue

<template>
  <div>
    <h2>Right--数据接收方--num的值为:{{ num }}</h2>
  </div>
</template>
<script>
import bus from './eventBus.js'
export default {
  name: 'MyRight',
  data() {
    return {
      num: 0,
    }
  },
  created() {
    bus.on('countChange', count => {
      this.num = count
    })
  }
}
</script> 

效果:

d70d2de5e23cc2adc25eb1f9fb03762a.png

后代关系组件之间的数据共享-provide&inject

作用:跨层级共享数据


场景:

378c483c55b6bca5e667f61df7e59582.png

后代关系组件之间共享数据,指的是父节点的组件向其子孙组件共享数据。此时组件之间的嵌套关系比较复杂,可以使用 provide 和 inject 实现后代关系组件之间的数据共享。


语法:


1-父组件 provide提供数据

export default {
  provide () {
    return {
       // 普通类型【非响应式】
       color: this.color, 
       // 复杂类型【响应式】
       userInfo: this.userInfo, 
    }
  }
}

2-子/孙组件 inject 获取数据

export default {
  inject: ['color','userInfo'],
  created () {
    console.log(this.color, this.userInfo)
  }
}

图示:

0a081e2691095ea49c18ff178ef69eaf.png

注意:


1-provide提供的简单类型的数据不是响应式的,复杂类型数据是响应式。(推荐提供复杂类型数据)如上图所示,如果我用button修改color,那么图中元素不会有变动,而用button修改userInfo中的数据,则图中相应元素会有变动。


2-子/孙组件通过inject获取的数据,不能在自身组件内修改


补充:2021版的Vue课程


父节点对外共享响应式的数据


值得注意的是,provide中return回去的数据,并非是响应式的数据,即若我在父组件中用button修改p标签的颜色,子组件的中的p标签颜色不会跟着一块变。


父节点使用 provide 向下共享数据时,可以结合 computed 函数向下共享响应式的数据。示例代码如下:

432a06c3d10ba41a0ed3de51d2ad850c.png

子孙节点使用响应式的数据


如果父级节点共享的是响应式的数据,则子孙节点必须以 .value 的形式进行使用。示例代码如下:

5572046716ae085eb1c0d0bde6bbc925.png

vuex

vuex 是终极的组件之间的数据共享方案。在企业级的 vue 项目开发中,vuex 可以让组件之间的数据共享变得高效、清晰、且易于维护。


个人总结:

父子关系


① 父 -> 子 属性绑定

② 子 -> 父 事件绑定

③ 父 <-> 子 组件上的 v-model


兄弟关系


④ EventBus


后代关系


⑤ provide & inject


全局数据共享


⑥ vuex


组件的 props

为了提高组件的复用性,在封装 vue 组件时需要遵守如下的原则:


组件的 DOM 结构、Style 样式 要尽量复用


组件中要展示的数据,尽量由组件的使用者提供


为了方便使用者为组件提供要展示的数据,vue 组件提供了 props 的概念。


基础概念:

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


作用:父组件通过 props 向子组件传递要展示的数据


特点:可以传递 任意数量与类型 的prop ;提高了组件的复用性


语法:简易写法


子组件接收

props: ['数据1', '数据2']

举例:


传递父组件中的数据到子组件中


使用 v-bind 属性绑定的形式,为组件动态绑定 props 的值

34891f777126ef6338492aa5ec99b332.png

注意: :username=”username” 左边是子,右边是父


效果:

b0b87b8b618b52efc2524d5f8e6cc6e1.png

Code:


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 {
  props: ['username', 'age', 'isSingle', 'car', 'hobby']
}
</script>
<style>
.userinfo {
  width: 300px;
  border: 3px solid #000;
  padding: 20px;
}
.userinfo > div {
  margin: 20px 10px;
}
</style>

props校验

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


语法:


类型校验(最常用)

props: {
  校验的属性名:类型    // Number String Boolean ...
}

▪非空校验

▪默认值

▪自定义校验

举例:进度条的进度只能传入数字而不能是其他的数据类型

6a8703ed228f7011d77194904ea3ed19.png

BaseProgress.vue 子组件接收父组件的数据

export default {
  // 1.基础写法(类型校验)
  props: {
    w: Number,
  },
}

props校验完整写法

类型校验是最常用的,如果你需要后面几种校验,就需要补充以下的写法:


语法:

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

代码示例:

<script>
export default {
  // 完整写法(类型、默认值、非空、自定义校验)
  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
        }
      },
    },
  },
}
</script>

注意:


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


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


props&data、单向数据流

**共同点:**都可以给组件提供数据


区别:


data 的数据是自己的 → 随便改

prop 的数据是外部的 → 不能直接改,要遵循 单向数据流


单向数据流:


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


子若想影响父的数据,需要通过$.emit来影响父。然后,父在将数据单向流动给子。

1b20ea68e9d153ee5fab4aea85c80646.png

口诀:谁的数据谁负责

案例:子接收父Count值,并且子通过 this.$emit 传递方法给父,让其修改数据

61571646a50c78b545918d776fa8b0bf.png

子想要改变父数据,需要通过 this.$emit 进行传递数据


父接收子changeCount方法,并利用 handleChange 接收数据,从而修改自身Count值

0e5b030dce6e58855e4fe91d73106c12.png



注意:@changeCount=”handleChange” 左边是子,右边是父


Code:


App.vue

<template>
  <div class="app">
    <BaseCount
      @changeCount="handleChange"
      :count="count"
    ></BaseCount>
  </div>
</template>
<script>
import BaseCount from './components/BaseCount.vue'
export default {
  components:{
    BaseCount
  },
  data(){
    return {
      count:100
    }
  },
  methods:{
    handleChange(newCount) {
      this.count = newCount
    }
  }
}
</script>
<style>
</style>

BaseCount.vue

<template>
  <div class="base-count">
    <button @click="handleSub()">-</button>
    <span>{{ count }}</span>
    <button @click="handleAdd()">+</button>
  </div>
</template>
<script>
export default {
  // 1.自己的数据随便修改  (谁的数据 谁负责)
  // data () {
  //   return {
  //     count: 100,
  //   }
  // },
  // 2.外部传过来的数据 不能随便修改
  props: {
    count: Number,
  },
  methods: {
    handleAdd() {
      this.$emit('changeCount', this.count + 1)
    },
    handleSub() {
      this.$emit('changeCount', this.count - 1)
    }
  }
}
</script>
<style>
.base-count {
  margin: 20px;
}
</style>

props 的大小写命名

组件中如果使用“camelCase (驼峰命名法)”声明了 props 属性的名称,


则有两种方式为其绑定属性的值:

022ea6fd65275f26b25cf47fabe193ea.png

理解:


封装的时候采用驼峰命名法,那么外界在传递属性的时候既可以通过短横线命名,也可以通过驼峰命名法命名


注意:


如果我们在组件命名属性时采用驼峰命名法,

<script>
export default {
  name: 'MyArticle',
  // 外界可以传递指定的数据,到当前的组件中
  props: ['author', 'title', 'MyTest']
}
</script>

那么,在传递属性时,我们既可以使用驼峰命名法,也可以使用短横线命名法

<my-article :title="info.title" :author="info.author" :MyTest="info.MyTest"></my-article>

案例:小黑记事本-组件版

案例效果:

07e49fa5867d09f36472072150d5b46e.png

需求说明:

拆分基础组件

渲染待办任务

添加任务

删除任务

底部合计 和 清空功能

持久化存储


拆分基础组件:


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


思路:

64bb0a9fbde27e57615f7a13b9d05e6b.png

具体操作:


1-拆分并渲染

/**
 * 渲染功能:
 * 1. 子组件提供数据给父组件
 * 2. 父传数据给子
 * 3. 利用 v-for 渲染数据
 */

Code:


App.vue

<template>
  <!-- 主体区域 -->
  <section id="app">
    <TodoHeader></TodoHeader>
    <TodoMain :list="list"></TodoMain>
    <TodoFooter></TodoFooter>
  </section>
</template>
<script>
import TodoHeader from './components/TodoHeader.vue'
import TodoMain from './components/TodoMain.vue'
import TodoFooter from './components/TodoFooter.vue'
/**
 * 渲染功能:
 * 1. 子组件提供数据给父组件
 * 2. 父传数据给子
 * 3. 利用 v-for 渲染数据
 */
export default {
  components: {
    TodoHeader,
    TodoMain,
    TodoFooter,
  },
  data () {
    return {
      list: [
        { id: 1, name: '打篮球1'},
        { id: 2, name: '打篮球1'},
        { id: 3, name: '打篮球1'}
      ]
    }
  }
}
</script>
<style>
</style>

TodoMain.vue

<template>
  <div>
      <!-- 列表区域 -->
      <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" ></button>
          </div>
        </li>
      </ul>
    </section>
  </div>
</template>
<script>
export default {
  name: 'TodoMain',
  data() {
    return {
    }
  },
  props: {
    list: Array
  },
  methods: {
  },
}
</script>
<style>
</style>

2-添加功能(添加、删除、统计、清空、持久化存储)

/**
 * 添加功能:
 * 1. 收集表单数据 v-model
 * 2. 监听事件(回车+点击 都要进行添加)
 * 3. 子传父,将任务名称传递给父组件
 * 4. 父组件进行添加 unshift(自己的数据自己负责)
 */
/**
 * 删除功能:
 * 1. 监听时间(监听删除的点击)携带id
 * 2. 子传父,将删除的id传递给父组件App.vue
 * 3. 进行删除 filter (自己的数据自己负责)
 */
// 底部合计:父组件传递list到底部组件  —>展示合计
// 清空功能:监听事件 —> **子组件**通知父组件 —>父组件清空
// 持久化存储: watch监听数据变化,持久化到本地

Code:


App.vue

<template>
  <!-- 主体区域 -->
  <section id="app">
    <TodoHeader @add="handleAdd"></TodoHeader>
    <TodoMain :list="list" @del="handleDel"></TodoMain>
    <TodoFooter :total="this.list.length" @clear="handleClear"></TodoFooter>
  </section>
</template>
<script>
import TodoHeader from './components/TodoHeader.vue'
import TodoMain from './components/TodoMain.vue'
import TodoFooter from './components/TodoFooter.vue'
/**
 * 渲染功能:
 * 1. 子组件提供数据给父组件
 * 2. 父传数据给子
 * 3. 利用 v-for 渲染数据
 */
/**
 * 添加功能:
 * 1. 收集表单数据 v-model
 * 2. 监听事件(回车+点击 都要进行添加)
 * 3. 子传父,将任务名称传递给父组件
 * 4. 父组件进行添加 unshift(自己的数据自己负责)
 */
/**
 * 删除功能:
 * 1. 监听时间(监听删除的点击)携带id
 * 2. 子传父,将删除的id传递给父组件App.vue
 * 3. 进行删除 filter (自己的数据自己负责)
 */
/**
 * 持久化存储: watch监听数据变化,持久化到本地
 */
const defaultList = [
  { id: 1, name: '打篮球1'},
  { id: 2, name: '打篮球1'},
  { id: 3, name: '打篮球1'}
]
export default {
  components: {
    TodoHeader,
    TodoMain,
    TodoFooter,
  },
  data () {
    return {
      list: JSON.parse(localStorage.getItem('list')) || defaultList,
    }
  },
  methods: {
    handleAdd(todoName) {
      this.list.unshift({
        id: +new Date(),
        name: todoName,
      })
    },
    handleDel(id) {
      // console.log(id);
      this.list = this.list.filter(item => item.id !== id)
    },
    handleClear() {
      this.list = []
    }
  },
  watch: {
    list: {
      deep: true,
      handler(newValue) {
        localStorage.setItem('list', JSON.stringify(newValue))
      }
    }
  }
}
</script>
<style>
</style>

TodoHeader.vue

<template>
  <div>
    <!-- 输入框 -->
    <header class="header">
      <h1>小黑记事本</h1>
      <input placeholder="请输入任务" class="new-todo" v-model="todoName" @keyup.enter="handleAdd"/>
      <button class="add" @click="handleAdd" >添加任务</button>
    </header>
  </div>
</template>
<script>
export default {
  name: 'TodoHeader',
  data() {
    return {
      todoName: '',
    }
  },
  methods: {
    handleAdd() {
      if(this.todoName.trim() === '') {
        alert("请输入内容!")
        return
      }
      this.$emit('add', this.todoName)
      this.todoName = ''
    },
  },
}
</script>
<style>
</style>

TodoMain.vue

<template>
  <div>
      <!-- 列表区域 -->
      <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>
  </div>
</template>
<script>
export default {
  name: 'TodoMain',
  data() {
    return {
    }
  },
  props: { 
    list: Array
  },
  methods: {
    handleDel(id) {
      // console.log(id);
      this.$emit("del", id)
    }
  },
}
</script>
<style>
</style>

TodoFooter.vue

<template>
  <div>
    <!-- 统计和清空 -->
    <footer class="footer">
      <!-- 统计 -->
      <span class="todo-count">合 计:<strong> {{ total }} </strong></span>
      <!-- 清空 -->
      <button class="clear-completed" @click="clear">
        清空任务
      </button>
    </footer>
  </div>
</template>
<script>
export default {
  name: 'TodoFooter',
  data() {
    return {
    }
  },
  props: {
    total: Number,
  },
  methods: {
    clear() {
      this.$emit('clear')
    }
  },
}
</script>
<style>
</style>

v-model 原理

基本原理

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


结合这段代码理解:

f303fd8bfbaf2c3fc928b363fcc80bea.png

<template>
  <div id="app" >
    <input v-model="msg" type="text">
    <input :value="msg" @input="msg = $event.target.value" type="text">
  </div>
</template>

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


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


数据变,视图跟着变 :value

视图变,数据跟着变 @input


代码实例:两个input是同步进退的

f166ef50519b2e0dfcd79c0e3f731d27.png

<template>
  <div class="app">
    <input type="text" v-model="msg1"/>
    <br />
    <input type="text" :value="msg1" @input="msg=$event.target.value">
  </div>
</template>
<script>
export default {
  data() {
    return {
      msg1: '',
    }
  },
}
</script>
<style>
</style>

v-model使用在其他表单元素上的原理


不同的表单元素, v-model在底层的处理机制是不一样的。比如给checkbox使用v-model底层处理的是 checked属性和change事件。


不过咱们只需要掌握应用在文本框上的原理即可


表单类组件封装

目标:实现子组件和父组件数据的双向绑定


案例:实现App.vue中的selectId和子组件选中的数据进行双向绑定

cee283c5a34c9dacb1c62feb103726ec.png

App.vue

<template>
  <div class="app">
    <BaseSelect
      :cityId = "selectId"
      @changeId="selectId = $event" //用 $event 表示当前形参
    ></BaseSelect>
  </div>
</template>
<script>
import BaseSelect from './components/BaseSelect.vue'
export default {
  data() {
    return {
      selectId: '104',
    }
  },
  components: {
    BaseSelect,
  },
}
</script>
<style>
</style>

BaseSelect.vue

<template>
  <div>
    <select :value="cityId" @change="handleChange">
      <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: {
    // e 指触发事件的事件源
    handleChange(e) {
      console.log(e.target.value);
      this.$emit('changeId', e.target.value)
    }
  }
}
</script>
<style>
</style>

v-model 简化代码

**目标:**父组件通过v-model 简化代码,实现子组件和父组件数据 双向绑定


简化:


v-model其实就是 :value和@input事件的简写


子组件:props通过value接收数据,事件触发 input

父组件:v-model直接绑定数据


案例:


子组件

<select :value="value" @change="handleChange">...</select>
props: {
  value: String
},
methods: {
  handleChange (e) {
    // 将这里的handleChange改成input
    this.$emit('input', e.target.value)
  }
}

父组件

<BaseSelect v-model="selectId"></BaseSelect>

总结:

993f512387d91dca6610cc84502d4d2d.png

可以结合上面的 父子组件之间数据 的双向同步


.sync修饰符(建议对比理解前几个双向绑定)

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


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

特点:prop属性名,可以自定义,非固定为 value (这与v-model不同)


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


理解:如果封装的不是value,而是这种弹框类的组件,建议用.sync建立双向绑定

79b7dae5b883fd1854a9075221c06495.png

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


语法


父组件

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

子组件

props: {
  visible: Boolean
},
this.$emit('update:visible', false)

案例:

a14c9ca4f63d3fe1a33e235f9a0897b1.png

Code:


App.vue

<template>
  <div class="app">
    <button
      @click="isShow = true"
    >退出按钮</button>
    <BaseDialog
      :visible.sync="isShow"
    ></BaseDialog>
  </div>
</template>
<script>
import BaseDialog from "./components/BaseDialog.vue"
export default {
  data() {
    return {
      isShow: false
    }
  },
  methods: {
  },
  components: {
    BaseDialog,
  },
}
</script>
<style>
</style>

BaseDialog.vue

<template>
  <div class="base-dialog-wrap" v-show="visible">
    <div class="base-dialog">
      <div class="title">
        <h3>温馨提示:</h3>
        <button class="close" @click="close">x</button>
      </div>
      <div class="content">
        <p>你确认要退出本系统么?</p>
      </div>
      <div class="footer">
        <button>确认</button>
        <button>取消</button>
      </div>
    </div>
  </div>
</template>
<script>
export default {
  props: {
    visible: Boolean,
  },
  methods: {
    close() {
      this.$emit('update:visible', 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>

ref和$refs

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


理解:每个 vue 的组件实例上,都包含一个 $refs 对象,里面存储着对应的 DOM 元素或组件的引用。默认情况下,组件的 $refs 指向一个空对象。


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


技术诞生原因:


如果使用querySelector进行查找图标.box, 可能在整个页面找到多个.box。


因此,为了更加精准地获取DOM元素,就需要ref与$refs这种技术

b06d569f7db08d652cf400ebe653f8ca.png

语法


1.给要获取的盒子添加ref属性

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

2.获取时通过 r e f s 获取 t h i s . refs获取 this.refs获取this.refs.chartRef 获取

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

注意


之前只用document.querySelect(‘.box’) 获取的是整个页面中的盒子


案例-1:获取DOM元素

8fae3780f56f7778f5eec2ffd8280103.png

Code:


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: 200px;
  height: 100px;
}
</style>

BaseChart.vue

<template>
  <div ref="mychart" class="base-chart-box">子组件</div>
</template>
<script>
import * as echarts from 'echarts'
export default {
  mounted() {
    // 基于准备好的dom,初始化echarts实例
    const myChart = echarts.init(this.$refs.mychart)
    // 绘制图表
    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>

案例-2:获取组件实例

5d5c8e79552abdb1819e673eb0dae31d.png

Code:


App.vue

<template>
  <div class="app">
    <h4>父组件 -- <button>获取组件实例</button></h4>
    <BaseForm ref="baseForm"></BaseForm>
    <button @click="handleGet">父-获取数据</button>
    <button @click="handleReset">父-重置数据</button>
  </div>
</template>
<script>
import BaseForm from './components/BaseForm.vue'
export default {
  components: {
    BaseForm,
  },
  methods: {
    handleGet() {
      this.$refs.baseForm.getFormData()
    },
    handleReset() {
      this.$refs.baseForm.resetFormData()
    }
  }
}
</script>
<style>
</style>

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: {
    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>

异步更新 & $nextTick

需求:


编辑标题, 编辑框自动聚焦


1.点击编辑,显示编辑框

2.让编辑框,立刻获取焦点

0fc05a34803dcf4c08641d1063bc8748.png

**代码实现:

<template>
  <div class="app">
    <div v-if="isShowEdit">
      <input type="text" v-model="editValue" ref="inp" />
      <button>确认</button>
    </div>
    <div v-else>
      <span>{{ title }}</span>
      <button @click="editFn">编辑</button>
    </div>
  </div>
</template>
<script>
export default {
  data() {
    return {
      title: '大标题',
      isShowEdit: false,
      editValue: '',
    }
  },
  methods: {
    editFn() {
        // 显示输入框
        this.isShowEdit = true  
        // 获取焦点
        this.$refs.inp.focus() 
    }  },
}
</script>

问题:


“显示之后”,立刻获取焦点是不能成功的!


原因:Vue 是异步更新DOM (提升性能)


解决方案


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


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


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

注意:$nextTick 内的函数体 一定是箭头函数,这样才能让函数内部的this指向Vue实例


补充:用setTimeout也能实现,但是它的时间没有$nextTick精准


解决代码:


Code:


App.vue

<template>
  <div class="app">
    <div v-if="isShowEdit">
      <input type="text" v-model="editValue" ref="inp" />
      <button>确认</button>
    </div>
    <div v-else>
      <span>{{ title }}</span>
      <button @click="handleEidt">编辑</button>
    </div>
  </div>
</template>
<script>
export default {
  data() {
    return {
      title: '大标题',
      isShowEdit: false,
      editValue: '',
    }
  },
  methods: {
   handleEidt() {
    //1. 显示输入框
    this.isShowEdit = true
    //2. 让输入框获取焦点
    this.$nextTick(() => {
      this.$refs.inp.focus()
    })
   }
  },
}
</script>
<style>
</style>

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: {
    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>

总结:

43c6baa9158c632ad6dd02a2ade92bd4.png


组件的生命周期

组件运行的过程:

fb6c4be3eae461d5d860ecc1bc9c9e4f.png

组件的生命周期指的是:组件从创建 -> 运行(渲染) -> 销毁的整个过程,强调的是一个时间段


监听组件的不同时刻的方式


vue 框架为组件内置了不同时刻的生命周期函数,生命周期函数会伴随着组件的运行而自动调用。


例如:


① 当组件在内存中被创建完毕之后,会自动调用 created 函数


② 当组件被成功的渲染到页面上之后,会自动调用 mounted 函数


③ 当组件被销毁完毕之后,会自动调用 unmounted 函数


案例:


Code:


App.vue

<template>
  <div>
    <h1>App 根组件</h1>
    <hr/>
    <life-cycle v-if="flag"></life-cycle>
    <button @click="flag = !flag">Toggle</button>
  </div>
</template>
<script>
import LifeCycle from './LifeCycle.vue'
export default {
  name: 'MyApp',
  components: {
    LifeCycle
  },
  data() {
    return {
      flag: false,
    }
  },
}
</script>
<style>
</style>

LifeCycle.vue

<template>
  <div>
    <h2>LifeCycle</h2>
  </div>
</template>
<script>
export default {
  name: 'LifeCycle',
  created() {
    console.log('组件在内存中被创建完毕了');
  },
  mounted() {
    console.log('组件被成功渲染到页面上了');
  },
  unmounted() {
    console.log('组件被销毁完毕了');
  }
}
</script>
<style>
</style>

理解:代码中的created mounted() unmounted函数放到子组件LifeCycle中,当子组件创建完毕之后,会调用created函数,当组件被渲染到页面上后,会调用mounted函数,当组件被销毁完毕之后,会调用unmounted函数。


监听组件的更新的方式


当组件的 data 数据更新之后,vue 会自动重新渲染组件的 DOM 结构,从而保证 View 视图展示的数据和Model 数据源保持一致。


当组件被重新渲染完毕之后,会自动调用 updated 生命周期函数。


案例:


Code:

<template>
  <div>
    <h2>LifeCycle</h2>
    <p>{{ count }}</p>
    <button @click="count += 1">+1</button>
  </div>
</template>
export default {
  name: 'LifeCycle',
  data() {
    return {
      count: 0,
    }
  },
  updated() {
    console.log('组件被重新渲染完毕了');
  },
}

组件中主要的生命周期函数

ed5c8cc781de22f1aab9952eb7d95022.png

注意:在实际开发中,created 是最常用的生命周期函数!


组件中全部的生命周期函数

94e731ab6997aef12f967b42cba357c4.png

疑问:为什么不在 beforeCreate 中发 ajax 请求初始数据


发起Ajax请求最好都在creat中


完整的生命周期图示


可以参考 vue 官方文档给出的“生命周期图示”,进一步理解组件生命周期执行的过程:

https://www.vue3js.cn/docs/zh/guide/instance.html#生命周期图示


vue 3.x 中全局配置 axios

1.为什么要全局配置 axios

在实际项目开发中,几乎每个组件中都会用到 axios 发起数据请求。此时会遇到如下两个问题:


① 每个组件中都需要导入 axios(代码臃肿)


② 每次发请求都需要填写完整的请求路径(不利于后期的维护)

e4ba0e283a8057654ca40a2d644ff621.png

2. 如何全局配置 axios


在 main.js 入口文件中,通过 app.config.globalProperties 全局挂载 axios,示例代码如下:

928f751d7749ace4cdf7f1a90935bf59.png

Code:


main.js

const app = createApp(MyApp)
axios.defaults.baseURL = 'https://www.escook.cn' //为axios配置请求的根路径
//将axios挂载为app的全局自定义属性之后,
//每个组件可以通过this直接访问到全局挂载的自定义属性
app.config.globalProperties.$http = axios

GetInfo:

export default {
  name: 'GetInfo',
  methods: {
    async getInfo() {
      const { data: res } = await this.$http.get('/api/get', {
        params: {
          name: 'ls',
          age: 33,
        },
      })
      console.log(res)
    },
  },
}

PostInfo:

export default {
  name: 'PostInfo',
  methods: {
    async postInfo() {
      const { data: res } = await this.$http.post('/api/post', { name: 'zs', age: 20 })
      console.log(res)
    },
  },
}
相关文章
|
2月前
|
JavaScript
在 Vue 中处理组件选项与 Mixin 选项冲突的详细解决方案
【10月更文挑战第18天】通过以上的分析和探讨,相信你对在 Vue 中使用 Mixin 时遇到组件选项与 Mixin 选项冲突的解决方法有了更深入的理解。在实际开发中,要根据具体情况灵活选择合适的解决方案,以确保代码的质量和可维护性。
107 7
|
19天前
|
缓存 JavaScript UED
Vue3中v-model在处理自定义组件双向数据绑定时有哪些注意事项?
在使用`v-model`处理自定义组件双向数据绑定时,要仔细考虑各种因素,确保数据的准确传递和更新,同时提供良好的用户体验和代码可维护性。通过合理的设计和注意事项的遵循,能够更好地发挥`v-model`的优势,实现高效的双向数据绑定效果。
120 64
|
19天前
|
前端开发 JavaScript 测试技术
Vue3中v-model在处理自定义组件双向数据绑定时,如何避免循环引用?
Web 组件化是一种有效的开发方法,可以提高项目的质量、效率和可维护性。在实际项目中,要结合项目的具体情况,合理应用 Web 组件化的理念和技术,实现项目的成功实施和交付。通过不断地探索和实践,将 Web 组件化的优势充分发挥出来,为前端开发领域的发展做出贡献。
27 8
|
19天前
|
JavaScript
在 Vue 3 中,如何使用 v-model 来处理自定义组件的双向数据绑定?
需要注意的是,在实际开发中,根据具体的业务需求和组件设计,可能需要对上述步骤进行适当的调整和优化,以确保双向数据绑定的正确性和稳定性。同时,深入理解 Vue 3 的响应式机制和组件通信原理,将有助于更好地运用 `v-model` 实现自定义组件的双向数据绑定。
|
1月前
|
存储 JavaScript 开发者
Vue 组件间通信的最佳实践
本文总结了 Vue.js 中组件间通信的多种方法,包括 props、事件、Vuex 状态管理等,帮助开发者选择最适合项目需求的通信方式,提高开发效率和代码可维护性。
|
1月前
|
存储 JavaScript
Vue 组件间如何通信
Vue组件间通信是指在Vue应用中,不同组件之间传递数据和事件的方法。常用的方式有:props、自定义事件、$emit、$attrs、$refs、provide/inject、Vuex等。掌握这些方法可以实现父子组件、兄弟组件及跨级组件间的高效通信。
|
2月前
|
缓存 JavaScript UED
Vue 的动态组件与 keep-alive
【10月更文挑战第19天】总的来说,动态组件和 `keep-alive` 是 Vue.js 中非常实用的特性,它们为我们提供了更灵活和高效的组件管理方式,使我们能够更好地构建复杂的应用界面。深入理解和掌握它们,以便在实际开发中能够充分发挥它们的优势,提升我们的开发效率和应用性能。
47 18
|
1月前
|
缓存 JavaScript UED
Vue 中实现组件的懒加载
【10月更文挑战第23天】组件的懒加载是 Vue 应用中提高性能的重要手段之一。通过合理运用动态导入、路由配置等方式,可以实现组件的按需加载,减少资源浪费,提高应用的响应速度和用户体验。在实际应用中,需要根据具体情况选择合适的懒加载方式,并结合性能优化的其他措施,以打造更高效、更优质的 Vue 应用。
|
2月前
|
前端开发 UED
vue3知识点:Suspense组件
vue3知识点:Suspense组件
38 4
|
2月前
|
JavaScript 前端开发 测试技术
组件化开发:创建可重用的Vue组件
【10月更文挑战第21天】组件化开发:创建可重用的Vue组件
27 1