Vue2:组件高级(上)

Vue2:组件高级(上)

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


目标:

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

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

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

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

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

npm i [email protected] -S


组件之间的样式冲突

样式冲突问题:

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

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

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

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

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

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

Untitled



style 节点的 scoped 属性

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

Untitled

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

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

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

原理

  1. 当前组件内标签都被添加data-v-hash值 的属性
  2. css选择器都被添加 [data-v-hash值] 的属性选择器

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

Untitled



/deep/ 样式穿透

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

Untitled

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

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

<style lang="less" scoped>
  p {
    color: red;
  }

  :deep(h3) {
    color: blue
  }
</style>



data必须是一个函数

原因

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

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

Untitled

举例

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

Untitled




组件通信

基础概念:

基础概念

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

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

组件之间的关系

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

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

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

A和DGH属于后代关系

Untitled



父子组件之间的数据共享

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

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

通信过程:

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

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

Untitled


父向子组件共享数据

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

Untitled

案例:

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>

效果:

Untitled


子向父组件共享数据

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

具体步骤:

子组件:

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

父组件:

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

Untitled

案例:

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>

效果:

Untitled


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

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

具体步骤:

  1. 父组件向子组件的props中传递数据
    1. 这里通过 v-model 方式进行双向数据绑定,维护组件两方数据同步
  2. 子组件声明emits属性,组件内的元素需要以 update: 的方式开头,这里需要更新哪个数据,就把相应数据的值丢过来,比如number
    1. 通过 $emits 的方式将数据发送出去

Untitled

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

案例:

  • 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>
    

效果:

Untitled



兄弟组件之间的数据共享

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', '今天天气不错,适合旅游')
    },
  },
}

图示

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

Untitled

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

Untitled

  • 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 对象,从而实现兄弟组件之间的数据共享。

示意图如下:

Untitled

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

3.1 安装 mitt 依赖包

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

npm install [email protected]

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> 
    

效果:

Untitled



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

作用:跨层级共享数据

场景

Untitled

后代关系组件之间共享数据,指的是父节点的组件向其子孙组件共享数据。此时组件之间的嵌套关系比较复杂,可以使用 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)
  }
}

图示

Untitled

注意:

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

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


补充:2021版的Vue课程

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

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

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

Untitled

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

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

Untitled



vuex

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


个人总结:

父子关系

① 父 -> 子 属性绑定
② 子 -> 父 事件绑定
③ 父 <-> 子 组件上的 v-model

兄弟关系

④ EventBus

后代关系

⑤ provide & inject

全局数据共享

⑥ vuex




组件的 props

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

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

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

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



基础概念:

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

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

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

语法:简易写法

子组件接收

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

举例

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

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

Untitled

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

效果:

Untitled

  • 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 ...
}
  • 非空校验
  • 默认值
  • 自定义校验

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

Untitled

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来影响父。然后,父在将数据单向流动给子。

Untitled

口诀:谁的数据谁负责

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

Untitled

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

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

Untitled

注意:@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 属性的名称,

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

Untitled

理解:

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

注意:

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

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

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

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



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

案例效果

Untitled

需求说明:

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

拆分基础组件:

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

思路:

Untitled

具体操作


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事件 的合写

结合这段代码理解:

Untitled

<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是同步进退的

Untitled

<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和子组件选中的数据进行双向绑定

Untitled

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>

总结

Untitled

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



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

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

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

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

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

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

Untitled

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

语法

父组件

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

子组件

props: {
  visible: Boolean
},

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

案例

Untitled

  • 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这种技术

Untitled

语法

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元素

Untitled

  • 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:获取组件实例

Untitled

  • 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. 让编辑框,立刻获取焦点

Untitled

**代码实现:

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

总结

Untitled




组件的生命周期

组件运行的过程:

Untitled

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

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

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('组件被重新渲染完毕了');
      },
    }
    

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

Untitled

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

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

Untitled

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

发起Ajax请求最好都在creat中

完整的生命周期图示

可以参考 vue 官方文档给出的“生命周期图示”,进一步理解组件生命周期执行的过程:
https://www.vue3js.cn/docs/zh/guide/instance.html#生命周期图示




vue 3.x 中全局配置 axios

  1. 为什么要全局配置 axios

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

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

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

Untitled

2. 如何全局配置 axios

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

Untitled

  • 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)
        },
      },
    }
    

猜你喜欢

转载自blog.csdn.net/CaptainDrake/article/details/132142677