vue3系统入门与项目实战学习笔记(上)

1,807 阅读7分钟

一、Vue语法初探

  • 本章中,将会通过编写实际例子,带你对 Vue 的语法有个粗浅的认知,让大家结合例子,有一些自己的疑问,从而带着问题继续学习,以便于更好的理解和掌握后面的知识点。

1.初学编写 HelloWorld 和 Counter

本章讲述了通过 cdn 的方式是如何使用 vue3.0

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>hollo world</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>
<body>
    <div id="root"></div>
</body>

<script>
    // 创建vue实例
    Vue.createApp({
        data(){
            return{
                content:1 // 定义变量
            }
        },
        mounted(){
            setInterval(() => {
                // 简写
                this.content += 1
                // 等同于
                // this.$data.content +=1
            }, 1000)
        },
        template: '<div>{{content}}</div>' // 在标签中使用变量
    }).mount('#root') // 将template中的内容挂载到id为root的元素中
</script>
</html>

2.编写字符串反转和内容隐藏功能

接下来用vue做两个简单的小 demo ,为了让初学者更好的理解 vue 语法

字符串反转功能

需求 : 用 vue 实现 点击按钮 时, 反转 文字内容,例如: 123 点击后反转成 321 的这么一个小效果。

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>反转字符串</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>
<body>
    <div id="root"></div>
</body>

<script>
    // 创建vue实例
    Vue.createApp({
        data(){
            return{
                content: 'hello world' // 定义变量
            }
        },
        methods: {
            handleBtnClick(){
                // 反转过后转换成字符串
                const newContent = this.content.split('').reverse().join('')
                // 重新赋值给页面元素
                this.content = newContent
            }
        },
        template: `
            <div>
                {{content}}
                <button v-on:click="handleBtnClick">反转</button>
            </div>
         `
        // 在标签中使用变量
    }).mount('#root') // 将template中的内容挂载到id为root的元素中
</script>
</html>

内容隐藏功能

需求 :通过 点击按钮 实现 显示隐藏 文字的 开关 的一个小功能。 index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>内容隐藏功能</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>
<body>
    <div id="root"></div>
</body>

<script>
    // 创建vue实例
    Vue.createApp({
        data(){
            return{
                show: true, // 定义变量控制显示隐藏
            }
        },
        methods: {
            handleBtnClick(){
               this.show = !this.show
            }
        },
        template: `
            <div>
                <span v-if="show">hello world</span>
                <button v-on:click="handleBtnClick">显示/隐藏</button>
            </div>
         `
        // 在标签中使用变量
    }).mount('#root') // 将template中的内容挂载到id为root的元素中
</script>
</html>

3.编写TodoList功能了解循环与双向绑定

本章节讲解了,如何使用 v-for 以及 v-model 。

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Todo List</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>
<body>
    <div id="root"></div>
</body>

<script>
    // 创建vue实例
    Vue.createApp({
        data(){
            return{
                inputValue: '',
                list: [
                    'hello',
                    'word',
                    'dell',
                    'lee'
                ]
            }
        },
        methods: {
            handleAddItem(){
                this.list.push(this.inputValue)
                this.inputValue = ''
            }
        },
        template: `
            <div>
                <input v-model="inputValue"/>
                <button v-on:click="handleAddItem">增加</button>
                <ul>
                    <li v-for="(item, index) of list">{{item}}{{index}}</li>
                </ul>
            </div>
         `
        // 在标签中使用变量
    }).mount('#root') // 将template中的内容挂载到id为root的元素中
</script>
</html>

4.组件概念初探,对 TodoList 进行组件代码拆分

本章节讲解了,如何使用 v-bind指令 以及 创建组件、 使用组件 的概念,案例如下:

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Todo List组件封装</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>
<body>
    <div id="root"></div>
</body>

<script>
    // 创建vue实例
   const app =  Vue.createApp({
        data(){
            return{
                inputValue: '',
                list: [
                    'hello',
                    'word',
                    'dell',
                    'lee'
                ]
            }
        },
        methods: {
            handleAddItem(){
                this.list.push(this.inputValue)
                this.inputValue = ''
            }
        },
        template: `
            <div>
                <input v-model="inputValue"/>
                <button v-on:click="handleAddItem" v-bind:title="inputValue">增加</button>
                <ul>
                    <todo-item v-for="(item, index) of list" v-bind:content="item" v-bind:index="index"/>
                </ul>
            </div>
         `
        // 在标签中使用变量
    }) 

    // 定义组件同时注册组件
    app.component('todo-item', { // 参数1:组件名称,参数2:组件配置
        props: ['content', 'index'], // 接收父组件传值         
        template: '<li>{{index}} -- {{content}}</li>'
    })

    // 1. 将template中的内容挂载到id为root的元素中
    // 2. 如果有组件,必须要先注册组件,不可直接链式写法创建实例(例如:Vue.createApp(...).mount('#root')),    然后直接挂载到页面元素执行mount,载到页面元素执行mount,
    // 那样的话页面已经初始化完成了,component就加不进去了,所以最后mount才可以
    app.mount('#root')
</script>
</html>

二、Vue基础语法

本章中,将会讲解 生命周期函数,指令,模版,数据,侦听器,事件,循环渲染 等基础语法知识点,帮助大家理解第一章重写过的代码,同时理解数据驱动的编程思想。

1.Vue 中应用和组件的基础概念

本章节讲解了, mvvm 设计模式 ,以及如何 修改外部根组件 。

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>组件的基础概念</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>
    // createApp 标识创建一个 Vue 应用,存储到 app 常量中
    // 传入的参数表示,这个应用最外层的组件,应该如何表示
    // mvvm 设计模式 m -> model 数据,v -> view 视图,vm -> viewModel 视图数据连接层
    const app = Vue.createApp({
        data(){ // model 层
            return{
                message: 'hello world'
            }
        },
        template: '<div>{{message}}</div>' // view 层
    }) 
    // vm 代表的就是 vue 应用的根组件,可以在f12控制台打印出来
    const vm = app.mount('#root')

    // 如果在f12控制台修改message数据也会发生变化,因为是双向数据绑定
    // vm.$data.message = '呵呵哒'
</script>

</html>

用 常量 保存 Vue实例 ,通过 vm.  的方式既可调用该实例下的 变量以及方法 。

2.理解 Vue 中的生命周期函数

image.png

本章节讲解了, vue 的 生命周期 的 执行顺序 ,vue 目前总共 8个生命周期 ,大家只需要记住: 生命周期,是在谋一时刻会自动执行的函数

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>vue的生命周期</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>
    // 生命周期函数:在谋一时刻会自动执行的函数
    const app = Vue.createApp({
        data(){
            return{
                message: 'hello world'
            }
        },
        // 1. 在实例生成之【前】会自动执行的函数
        beforeCreate(){ 
            console.log('beforeCreate') 
        },
        // 2. 在实例生成之【后】会自动执行的函数
        created(){
            console.log('created')
        },
        // 3. 在组件内容被渲染到页面之【前】立即执行的函数
        beforeMount(){
            console.log(document.getElementById('root').innerHTML, 'beforeMount')
        },
        // 4. 在组件内容被渲染到页面之【后】立即执行的函数
        mounted(){
            console.log(document.getElementById('root').innerHTML, 'mounted')
        },
        // 5. 当数据发生变化时会立即自动执行的函数
        beforeUpdate(){
            console.log(document.getElementById('root').innerHTML, 'beforeUpdate')
        },
        // 6. 当数据发生变化,页面重新渲染后,会自动执行的函数
        updated(){
            console.log(document.getElementById('root').innerHTML, 'updated')
        },
        // 7. 当 Vue 应用失效时, 自动执行的函数(被销毁时执行的函数,执行app.unmount()),从挂在的#root元素上解开绑定
        beforeUnmount(){
            console.log(document.getElementById('root').innerHTML, 'beforeUnmount')
        },
        // 8. 当 Vue 应用失效时,且 dom 完全销毁之后,自动执行的函数
        unmounted(){
            console.log(document.getElementById('root').innerHTML, 'unmounted')
        },
        methods: {
            handleItemClick(){
                alert('click')
            }
        },
        template: '<div v-on:click="handleItemClick">{{message}}</div>'
    }) 
    const vm = app.mount('#root')
</script>

</html>

如果 实例化 的 Vue根组件对象 中也可以不写 template 属性,可以直接在 元素内部 使用 表达式 的方式来做到同样效果,如下:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>vue的生命周期</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root">
        <div v-on:click="handleItemClick">{{message}}</div>
    </div>
</body>

<script>
    // 生命周期函数:在谋一时刻会自动执行的函数
    const app = Vue.createApp({
        data(){
            return{
                message: 'hello world'
            }
        },
        // 1. 在实例生成之【前】会自动执行的函数
        beforeCreate(){ 
            console.log('beforeCreate') 
        },
        // 2. 在实例生成之【后】会自动执行的函数
        created(){
            console.log('created')
        },
        // 3. 在组件内容被渲染到页面之【前】立即执行的函数
        beforeMount(){
            console.log(document.getElementById('root').innerHTML, 'beforeMount')
        },
        // 4. 在组件内容被渲染到页面之【后】立即执行的函数
        mounted(){
            console.log(document.getElementById('root').innerHTML, 'mounted')
        },
        // 5. 当数据发生变化时会立即自动执行的函数
        beforeUpdate(){
            console.log(document.getElementById('root').innerHTML, 'beforeUpdate')
        },
        // 6. 当数据发生变化,页面重新渲染后,会自动执行的函数
        updated(){
            console.log(document.getElementById('root').innerHTML, 'updated')
        },
        // 7. 当 Vue 应用失效时, 自动执行的函数(被销毁时执行的函数,执行app.unmount()),从挂在的#root元素上解开绑定
        beforeUnmount(){
            console.log(document.getElementById('root').innerHTML, 'beforeUnmount')
        },
        // 8. 当 Vue 应用失效时,且 dom 完全销毁之后,自动执行的函数
        unmounted(){
            console.log(document.getElementById('root').innerHTML, 'unmounted')
        },
        methods: {
            handleItemClick(){
                alert('click')
            }
        }
    }) 
    const vm = app.mount('#root')
</script>

</html>

3.常用模版语法讲解

  1. v-text 与 {{}}  :用来渲染纯文字
  2. v-html :用来渲染标签
  3. v-bind:动态属性
  4. v-once:元素中的变量只使用一次
  5. v-if :显示/隐藏重新渲染页面元素
  6. v-show :显示/隐藏元素,不重新渲染dom元素(display:none,来会切换显示隐藏)
  7. v-on :定义事件,例如:click 之类的
  8. 简写形式v-on 以及 v-bind 等等的简写
  9. 动态属性动态 给 属性名 或者 事件名 赋值
  10. 事件修饰符阻止冒泡事件 等等的一些 语法糖

v-text 与 {{}}

如果我们想在页面渲染一个 data 中的 变量 ,可以使用 v-text 如下:

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>常用模版语法讲解</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>
    // 生命周期函数:在谋一时刻会自动执行的函数
    const app = Vue.createApp({
        data(){
            return{
                message: 'hello world'
            }
        },
        template: '<div v-text="message"></div>'
    }) 
    const vm = app.mount('#root')
</script>

</html>

或者使用  {{}}

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>常用模版语法讲解</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>
    // 生命周期函数:在谋一时刻会自动执行的函数
    const app = Vue.createApp({
        data(){
            return{
                message: 'hello world'
            }
        },
        template: '<div>{{message}}</div>'
    }) 
    const vm = app.mount('#root')
</script>

</html>

v-text 与  {{}}  的区别:v-text :将数据解析为纯文本,不能输出真正的 html ,与 花括号 的区别是在页面加载时不显示  {{}}

v-html

如果我们想 动态 在 字符串中拼接标签 并且 展示在页面上 ,我们可以使用 v-html ,如下:

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>常用模版语法讲解</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>
    // 生命周期函数:在谋一时刻会自动执行的函数
    const app = Vue.createApp({
        data(){
            return{
                message: '<strong>hello world</strong>'
            }
        },
        template: '<div v-html="message"></div>'
    }) 
    const vm = app.mount('#root')
</script>

</html>

v-bind

如果我们想 动态给标签添加属性 时,可以使用 v-bind ,代码如下:

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>常用模版语法讲解</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>
    // 生命周期函数:在谋一时刻会自动执行的函数
    const app = Vue.createApp({
        data(){
            return{
                message: 'hello world'
            }
        },
        // template: '<div v-bind:title="message">{{message}}</div>'
        // 或者
        // template: '<div :title="message">{{message}}</div>'
        // 或者可以写一些表达式
        template: '<div :title="message">{{ message ? "1" : "0" }}</div>'
    }) 
    const vm = app.mount('#root')
</script>

</html>

v-once

需求 :只想展示第一次变量中展示的数据,以后 变量改变了,页面展示的数据也不跟随改变,可以使用 v-once ,它的意思是 元素中的变量只使用一次

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>常用模版语法讲解</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>
    // 生命周期函数:在谋一时刻会自动执行的函数
    const app = Vue.createApp({
        data(){
            return{
                message: 'hello world'
            }
        },
        template: '<div v-once>{{message}}</div>'
    }) 
    const vm = app.mount('#root')
</script>

</html>

v-if

应用场景:适用于 不频繁显示隐藏元素 的需求,如果 频繁显示隐藏 ,可使用 v-show 。

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>常用模版语法讲解</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>
    // 生命周期函数:在谋一时刻会自动执行的函数
    const app = Vue.createApp({
        data(){
            return{
                message: 'hello world',
                show: true
            }
        },
        template: '<div v-if="show">{{message}}</div>'
    }) 
    const vm = app.mount('#root')
</script>

</html>

v-show

应用场景:适用于 频繁显示隐藏

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>常用模版语法讲解</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>
    // 生命周期函数:在谋一时刻会自动执行的函数
    const app = Vue.createApp({
        data(){
            return{
                message: 'hello world',
                show: true
            }
        },
        template: '<div v-show="show">{{message}}</div>'
    }) 
    const vm = app.mount('#root')
</script>

</html>

v-on

应用场景:给元素添加事件时,例如: 给一个元素,添加click事件,代码如下:

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>常用模版语法讲解</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>
    // 生命周期函数:在谋一时刻会自动执行的函数
    const app = Vue.createApp({
        data(){
            return{
                message: 'hello world',
                show: true
            }
        },
        methods: {
            handleClick(){
                alert('click')
            }
        },
        template: '<div v-on:click="handleClick">{{message}}</div>'
    }) 
    const vm = app.mount('#root')
</script>

</html>

这里要注意,方法 一定要写在 methods 中。

简写形式

  1. v-on:click 可以写成  @click
  2. v-bind:title 可以写成  :title

动态属性

应用场景:假如说元素身上的 属性名或者事件名不确定 ,可以用一个 变量的形式来定义动态定义属性 ,代码如下:

index.htrml

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>常用模版语法讲解</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>
    // 生命周期函数:在谋一时刻会自动执行的函数
    const app = Vue.createApp({
        data(){
            return{
                message: 'hello world',
                show: true,
                name: 'title', // 属性名称
                event: 'click' // 事件名称
            }
        },
        methods: {
            handleClick(){
                alert('click')
            }
        },
        template: '<div @[event]="handleClick" :[name]="message">{{message}}</div>'
    }) 
    const vm = app.mount('#root') 
</script>

</html>

事件修饰符

应用场景 :以前 js 阻止冒泡默认事件 都用 event对象 下的自带方法,例如:e.preventDefault()  ,现在 vue 中有了新的方式 语法糖 解决同样问题,代码如下:

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>常用模版语法讲解</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>
    // 生命周期函数:在谋一时刻会自动执行的函数
    const app = Vue.createApp({
        data(){
            return{
                message: 'hello world',
                show: true,
                name: 'title', // 属性名称
                event: 'click' // 事件名称
            }
        },
        methods: {
            handleClick(){
                alert('click')
            }
        },
        template: `
        <form action="https://www.baidu.com" @click.prevent="handleClick">
            <button type="submit">提交</button>
        </form>
        `
    }) 
    const vm = app.mount('#root') 
</script>

</html>

4.数据,方法,计算属性和侦听器

本章将主要讲解data 、 methods 、 computed 、 watcher

data

之前使用的 data 中的数据,我们可以 通过控制台 来对 data 中的 数据进行修改 ,例如:

vm.$data.message = '123'

这样就可以把 data 中的数据 修改 了,还有一个更简便的方式

vm.message = '123'

这样也是可以修改的。

methods

定义方法 可以在 methods 中定义,但是这里需要注意 methods 中的方法的 this指向 ,都是指向vue的实例 ,定义方法时候不可以用箭头函数的方式定义方法,因为箭头函数的this指向的是外层对象的this 。

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>数据,方法,计算属性和侦听器</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>
    const app = Vue.createApp({
        data(){
            return{
                message: 'hello world'
            }
        },
        methods: {
            handleClick(){
                console.log('click', this.message)
            }
        },
        template: `
            <div @click="handleClick">{{message}}</div>
        `
    })  
    const vm = app.mount('#root') 
</script>

</html> 

methods 也可以这样用,这种用法叫做 插值表达式 ,如下:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>数据,方法,计算属性和侦听器</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>
    const app = Vue.createApp({
        data(){
            return{
                message: 'hello world'
            }
        },
        methods: {
            formatString(string){
                return string.toUpperCase()
            }
        },
        template: `
            <div>{{formatString(message)}}</div>
        `
    })  
    const vm = app.mount('#root') 
</script>

</html> 

computed

computed 用来 计算值,最终 返回一个计算后的结果

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>数据,方法,计算属性和侦听器</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>
    const app = Vue.createApp({
        data(){
            return{
                count: 2,
                price: 5
            }
        },
        computed: {
            total(){
                return this.count * this.price
            }
        },
        template: `
            <div>{{ total }}</div>
        `
    })  
    const vm = app.mount('#root') 
</script>

</html> 

那么有人说了用 methods 能实现与 computed 同样的效果,实际上它俩还是有不同的,

computed :当计算属性依赖的内容发生变更时,才会重新执行计算

methods只要页面重新渲染,才会重新计算

watch

应用场景监听数据变化时,做一些逻辑处理

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>数据,方法,计算属性和侦听器</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>
    // computed 和 method 都能实现的一个功能,建议使用 computed,因为有缓存
    // computed 和 watcher 都能实现的功能,建议使用 computed 因为更加简洁
    const app = Vue.createApp({
        data(){
            return{
                count: 2,
                price: 5,
                newTotal: 10
            }
        }, 
        watch:{
            // price 发生变化时,函数会执行
            price(current, prev){
                this.newTotal = current * this.count
            }
        },
        template: `
            <div>{{newTotal}}</div>
        `
    })  
    const vm = app.mount('#root') 
</script>

</html> 

5.样式绑定语法

记下来讲 2种 绑定样式的方法,动态class 以及 动态style 。

class绑定

在 vue 中有以下几种方式来 动态绑定class

  1. 字符串形式 : 通过字符串来控制样式的显示
  2. 对象形式 :通过对象来控制样式的显示
  3. 数组形式 :通过数组来控制样式的显示
  4. 组件上绑定class :通过 $attrs 来继承父组件样式

字符串形式

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>样式绑定语法</title>
    <style>
        .red{
            color: red;
        }
        .green{
            color: green;
        }
    </style>

    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>
    const app = Vue.createApp({
        data(){
            return{
                classString: 'red'
            }
        },
        template: `
            <div :class="classString">hello world</div>
        `
    })  
    const vm = app.mount('#root') 
</script>

</html> 

然后在 f12控制台 输入

vm.classString = 'green'

文字就会变成绿色 。

对象形式

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>样式绑定语法</title>
    <style>
        .red{
            color: red;
        }
        .green{
            color: green;
        }
    </style>

    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>
    const app = Vue.createApp({
        data(){
            return{
                classObject: {red:false, green:true}
            }
        },
        template: `
            <div :class="classObject">hello world</div>
        `
    })  
    const vm = app.mount('#root') 
</script>

</html> 

数组形式

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>样式绑定语法</title>
    <style>
        .red{
            color: red;
        }
        .green{
            color: green;
        }
    </style>

    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>
    const app = Vue.createApp({
        data(){
            return{
                classArray: ['red','green', { brown: true }]
            }
        },
        template: `
            <div :class="classArray">hello world</div>
        `
    })  
    const vm = app.mount('#root') 
</script>

</html> 

组件上绑定class

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>样式绑定语法</title>
    <style>
        .red{
            color: red;
        }
        .green{
            color: green;
        }
    </style>

    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>
    const app = Vue.createApp({
        data(){
            return{
                classString: 'red',
                classObject: {red:false, green:true},
                classArray: ['red','green', { brown: true }]
            }
        },
        template: `
            <div :class="classString">
                hello world
                <demo class="green"/>
            </div>
        `
    })   
    app.component('demo', {  
        template: `
            <div class="green">one</div>
        `
    })
    const vm = app.mount('#root') 
</script>

</html> 

如果 子组件中的template子级只有一个元素 ,可以在 父组件中子组件标签上添加class 改变 子组件样式 ,子组件template子级有多个元素 ,vue 会不知道到底是把 green 加到 第一个节点 ,还是 第二个节点 ,这种情况可以 直接在子组件标签身上添加class样式 ,如下

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>样式绑定语法</title>
    <style>
        .red{
            color: red;
        }
        .green{
            color: green;
        }
    </style>

    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>
    const app = Vue.createApp({
        data(){
            return{
                classString: 'red',
                classObject: {red:false, green:true},
                classArray: ['red','green', { brown: true }]
            }
        },
        template: `
            <div :class="classString">
                hello world
                <demo/>
            </div>
        `
    })   
    app.component('demo', {  
        template: `
            <div class="green">one</div>
            <div class="green">two</div>
        `
    })
    const vm = app.mount('#root') 
</script>

</html> 

再或者可以使用  $attrs,如下代码,这样的写法是什么意思呢,意思:子组件使用父组件属性上的class的值(子组件继承父组件样式)

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>样式绑定语法</title>
    <style>
        .red{
            color: red;
        }
        .green{
            color: green;
        }
    </style>

    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>
    const app = Vue.createApp({
        data(){
            return{
                classString: 'red',
                classObject: {red:false, green:true},
                classArray: ['red','green', { brown: true }]
            }
        },
        template: `
            <div :class="classString">
                hello world
                <demo class="green"/>
            </div>
        `
    })   
    app.component('demo', {  
        template: `
            <div :class="$attrs.class">one</div>
            <div :class="$attrs.class">two</div> 
        `
    })
    const vm = app.mount('#root') 
</script>

</html> 

style绑定

在 vue 中有以下几种方式来 动态绑定style

  1. 字符串形式通过字符串来控制样式的显示
  2. 对象形式通过对象形式来控制样式的显示

字符串形式

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>样式绑定语法</title>
    <style>
        .red{
            color: red;
        }
        .green{
            color: green;
        }
    </style>

    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>
    const app = Vue.createApp({
        data(){
            return{
                styleString: 'color:yellow'
            }
        },
        template: `
            <div :style="styleString">hello world</div>
        `
    })   
    const vm = app.mount('#root') 
</script>

</html> 

对象形式

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>样式绑定语法</title>
    <style>
        .red{
            color: red;
        }
        .green{
            color: green;
        }
    </style>

    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>
    const app = Vue.createApp({
        data(){
            return{
                styleObject: {
                    color: 'orange',
                    background: 'yellow'
                }
            }
        },
        template: `
            <div :style="styleObject">hello world</div>
        `
    })   
    const vm = app.mount('#root') 
</script>

</html> 

6.条件渲染

如果 频繁的显示隐藏 建议使用 v-show ,因为 v-if 会 重新渲染dom元素 ,以及 v-if 还有配套的 v-else 、v-else-if ,如下 demo :

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>条件渲染</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>
    const app = Vue.createApp({
        data(){
            return{
              show: false,
              condition: true
            }
        },
        template: `
            <div v-if="show">if</div>
            <div v-else-if="condition">else if</div>
            <div v-else>else</div>
        `
    })   
    const vm = app.mount('#root') 
</script>

</html> 

这种写法 if 要跟 else 或 else-if 使用的标签,贴在一起使用,不然会报错。

7.列表循环渲染

接下来介绍如何使用 v-for循环 数组 或 对象 。

循环数组

循环数组用有 2 个参数

参数1:item 每一项的值

参数2:index索引 ,index可选 。

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>列表循环渲染</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>
    const app = Vue.createApp({
        data(){
            return{
                list: ['dell', 'lee', 'teacher']
            }
        },
        template: `
            <div>
                <div v-for="(item, index) in list" :key="item">
                    {{ item }} -- {{ index }}
                </div>
            </div>
        `
    })   
    const vm = app.mount('#root') 
</script>

</html> 

循环对象

循环对象用有3 个参数

参数1:value每一项 键值对 的值 参数2:key 对象的 key 参数3:index 索引值

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>列表循环渲染</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>
    const app = Vue.createApp({
        data(){
            return{
                listObject: {
                    name: 'dell',
                    lastName: 'lee',
                    job: 'teacher'
                }
            }
        },
        template: `
            <div v-for="(value, key, index) in listObject" :key="value">
            {{value}} -- {{key}} -- {{index}}
            </div>
        `
    })   
    const vm = app.mount('#root') 
</script>

</html> 

循环数字

也可以 循环数字 ,如下:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>列表循环渲染</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>
    const app = Vue.createApp({
        template: `
            <div v-for="item in 10" :key="item">
                {{ item }}
            </div>
        `
    })   
    const vm = app.mount('#root') 
</script>

</html> 

v-fo 与 v-if 同时使用

需求 : 循环数组 或 对象 时,如果某一项不想展示,你可能会想 v-for 与 v-if 同时使用,这种做法是错误的, v-for 与 v-if 同时使用时,v-for 的权重要比 v-if 高,所以 v-if 不生效

解决 :

  1. 方案一:在内部添加一个 template标签(该标签渲染时不会占位) ,在 div 元素上使用 v-if 做 判断处理
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>列表循环渲染</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>
    const app = Vue.createApp({
        data(){
            return{
                listObject: {
                    name: 'dell',
                    lastName: 'lee',
                    job: 'teacher'
                }
            }
        },
        template: `
            <template v-for="(value, key, index) in listObject" :key="value">
                <div v-if="value !== 'dell'">
                    {{value}} -- {{key}} -- {{index}}
                </div>
            </template>
        `
    })   
    const vm = app.mount('#root') 
</script>

</html> 
  1. 方案二:用 计算属性过滤掉隐藏项 ,只返回 显示项
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>列表循环渲染</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>
    const app = Vue.createApp({
        data(){
            return{
                listObject: {
                    name: 'dell',
                    lastName: 'lee',
                    job: 'teacher'
                }
            }
        },
        computed: {
            // 把展示项过滤出来
            filterLsit(){
                let result = {}
                for(const [key,value] of Object.entries(this.listObject)){
                    if(value !== 'dell'){
                        result[key] = value
                    }
                }
                return result
            }
        },
        template: `
            <div v-for="(value, key, index) in filterLsit" :key="value">
                {{value}} -- {{key}} -- {{index}}
            </div>
        `
    })   
    const vm = app.mount('#root') 
</script>

</html> 

注意

使用 v-for循环 时,要添加 key属性 ,因为,向 循环的列表添加了一项数据,vue不会重新渲染整个列表,只会渲染最新添加的那项 ,为了做区分所以 加key属性做数据的区分 ,建议使用 item 或者一些唯一的值来做key ,不推荐使用 index ,因为 index 做 key 渲染列表时,如果有恰巧有 删除功能,删除了一项 item, 删除项 后的兄弟级会 自动顶到前面补位 , 就会 导致程序出现bug 。

8.事件绑定

  1. event问题 :event 的部分场景,如何使用
  2. 一个事件执行多个函数
  3. 事件修饰符 :解决 事件冒泡 等问题
  4. 按键修饰符: 回车键 、 tab键 等等
  5. 鼠标修饰符 : 鼠标对应的左中右按键修饰符
  6. 精确修饰符 : 组合按键一起按才会触发方法

event问题

如果我们要 额外传递参数 ,还要用到原生 event 对象时,在 实参 的地方传入一个  $event , 形参 的地方接收一下即可,如下代码:

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>事件绑定</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>
    const app = Vue.createApp({
        data(){
            return {
                counter: 0
            }
        },
        methods: {
            handleAddBtnClick(num, event){
                console.log(event.target)
                this.counter += num
            }
        },
        template: `
            {{counter}}
            <button @click="handleAddBtnClick(2, $event)">button</button>
        `
    })   
    const vm = app.mount('#root') 
</script>

</html> 

一个事件执行多个函数

在 vue 中有时候你想通过一个 点击事件同时执行2个或多个方法 时,你可以向下面代码一样,通过 逗号分隔 的形式来执行多个函数。

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>事件绑定</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>
    const app = Vue.createApp({
        methods: {
            handleAddBtnClick(){
                alert(1)
            },
            handleAddBtnClick2(){
                alert(2)
            }
        },
        template: `
            <button @click="handleAddBtnClick(),handleAddBtnClick2()">button</button>
        `
    })   
    const vm = app.mount('#root') 
</script>

</html> 

事件修饰符

1.stop修饰符

子容器 与 父容器 都有 点击事件 时,点击子容器,父容器的方法也会被触发 ,这种行为被称之为 事件冒泡 ,为了解决该问题, vue 中提供了 stop事件修饰符 来解决该问题。

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>事件绑定</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>
    const app = Vue.createApp({
        data(){
            return {
                counter: 0
            }
        },
        methods: {
            handleAddBtnClick(num, event){
                console.log(event.target)
                this.counter += num
            },
            handleDivClick(){ 
                alert('div click')
            }
        },
        template: `
            {{counter}}
            <div @click="handleDivClick">
                <button @click.stop="handleAddBtnClick(2, $event)">button</button>
            </div>
        `
    })   
    const vm = app.mount('#root') 
</script>

</html> 

2.self修饰符

在 父子嵌套 的结构中,只想 通过点击父级触发父级的事件,点击子级不触发父级的事件,可以通过 self修饰符 来解决此问题。

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>事件绑定</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>
    const app = Vue.createApp({
        data(){
            return {
                counter: 0
            }
        },
        methods: {
            handleAddBtnClick(num, event){
                console.log(event.target)
                this.counter += num
            },
            handleDivClick(){ 
                alert('div click')
            }
        },
        template: `
            <div @click.self="handleDivClick">
                {{counter}}
                    <button @click="handleAddBtnClick(2, $event)">button</button>
            </div>
        `
    })   
    const vm = app.mount('#root') 
</script>

</html> 

3.prevent修饰符

阻止默认行为修饰符

4.capture修饰符

捕获模式 : 冒泡模式 是从内到外,捕获模式 是从外到内,这个平时用的不多。

5.once修饰符

once: 事件只执行一次。

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>事件绑定</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>
    const app = Vue.createApp({
        methods: {
            handleDivClick(){ 
                alert('div click')
            }
        },
        template: `
            <div @click.once="handleDivClick">
                点击我
            </div>
        `
    })   
    const vm = app.mount('#root') 
</script>

</html> 

6.passive修饰符

passive修饰符 在使用  @scroll事件 时使用,性能会得到很大提升。

按键修饰符

vue 中除了 事件修饰符 还有 按键修饰符 ,这里就例举一些, enter 、tab 、delete 、 esc 、 up、 down 、left 、 right 这些修饰符都可以使用,可以通过这些 修饰符 用 keydown 进行一些按键的绑定。

1.enter修饰符

以前用 原生js 想实现 按回车键触发一个方法 ,就需要用 event 中的 keyCode 判断等不等 13 ,如果等于 13 就证明是 回车键 ,再执行该方法,vue 中可以用 enter按键修饰符 来解决此问题。

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>事件绑定</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>
    const app = Vue.createApp({
        methods: {
            // 原生写法想实现回车键就只能用event的keyCode === 13 代表是回车键
            handleKeyDown(){ 
                console.log('keydown')
            }
        },
        template: `
            <div>
                <input @keydown.enter="handleKeyDown"/>
            </div> 
        `
    })   
    const vm = app.mount('#root') 
</script>

</html> 

鼠标修饰符

鼠标修饰符 有 3 个:leftright 、middle ,接下來就拿 middle 举例:

1.middle修饰符

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>事件绑定</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>
    const app = Vue.createApp({
        methods: {
            handleClick(){ 
                console.log('handleClick')
            }
        },
        template: `
            <div>
                <div @click.middle="handleClick">点击</div>
            </div> 
        `
    })   
    const vm = app.mount('#root') 
</script>

</html> 

精确修饰符

精确修饰符 是什么意思呢,看下面代码,意思是 必须按住 ctrl 再按住鼠标触发 click 键时候才会执行要执行的函数

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>事件绑定</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>
    const app = Vue.createApp({
        methods: {
            handleClick(){ 
                console.log('handleClick')
            }
        },
        template: `
            <div>
                <div @click.ctrl="handleClick">点击</div>
            </div> 
        `
    })   
    const vm = app.mount('#root') 
</script>

</html> 

按住 ctrl + click ,此时再按住其他按键,也可以触发方法 ,有时候就是想 只按住这两个按键(ctrl + click)  触发方法,那应该如何解决呢,可以再加个  .exact修饰符

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>事件绑定</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>
    const app = Vue.createApp({
        methods: {
            handleClick(){ 
                console.log('handleClick')
            }
        },
        template: `
            <div>
                <div @click.ctrl.exact="handleClick">点击</div>
            </div> 
        `
    })   
    const vm = app.mount('#root') 
</script>

</html> 

9.表单中双向绑定指令的使用

input的双向数据绑定

下面的代码中,输入框的值改变后,页面上的文字也会改变,文字改变后,输入框中的值也会改变 ,这就是 双向数据绑定 ,如果用 input框的 v-model ,就可以不用写value 属性了,直接用它就可以了。

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>表单中双向绑定指令的使用</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>
    const app = Vue.createApp({
        data(){
            return{
                message: 'hello'
            }
        },
        template: `
            <div>
                {{message}}
                <input  v-model="message"/>
            </div> 
        `
    })   
    const vm = app.mount('#root') 
</script>

</html> 

textarea的双向数据绑定

向以前 textarea 都要用 双标签 来使用,像这样:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>表单中双向绑定指令的使用</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>
    const app = Vue.createApp({
        template: `
            <textarea>
                123
            </textarea>
        `
    })   
    const vm = app.mount('#root') 
</script>

</html> 

现在,在 vue中可以实现 双向数据绑定 了,而且只需要 单标签 的形式就可以使用。如下:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>表单中双向绑定指令的使用</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>

</body>

<script>
    const app = Vue.createApp({
        data(){
            return{
                message: 'hello'
            }
        },
        template: `
            <textarea v-model="message"/>
        `
    })   
    const vm = app.mount('#root') 
</script>

</html> 

checkbox的双向数据绑定

可以用 input 类型改为 checkbox ,然后绑定一个 布尔值的变量 即可实现 双向数据绑定 ,代码如下:

1.单个 checkbox

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>表单中双向绑定指令的使用</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>

</body>

<script>
    const app = Vue.createApp({
        data(){
            return{
                message: false
            }
        },
        template: `
            {{message}}
            <input type="checkbox" v-model="message"/>
        `
    })   
    const vm = app.mount('#root') 
</script>

</html> 

2.多个checkbox

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>表单中双向绑定指令的使用</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>

</body>

<script>
    const app = Vue.createApp({
        data(){
            return{
                message: []
            }
        },
        template: `
            {{message}}
            jack<input type="checkbox" v-model="message" value="jack"/>
            dell<input type="checkbox" v-model="message" value="dell"/>
            lee<input type="checkbox" v-model="message" value="lee"/>
        `
    })   
    const vm = app.mount('#root') 
</script>

</html> 

checkbox的自定义写法

checkbox 在 选中 以及 取消选中时候, 通过 true-value 、false-value 属性来 动态更改显示的文字 。

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>表单中双向绑定指令的使用</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>

</body>

<script>
    const app = Vue.createApp({
        data(){
            return{
                message: 'hello'
            }
        },
        template: `
            {{message}}
            <input type="checkbox" v-model="message" true-value="hello" false-value="world"/>
        `
    })   
    const vm = app.mount('#root') 
</script>

</html> 

radio的双向数据绑定

radio 储存值只需要用 默认空字符串 储存即可 ,不需要向 checkbox 那样用 数组 ,因为 radio 只能选择一个值 。

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>表单中双向绑定指令的使用</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>

</body>

<script>
    const app = Vue.createApp({
        data(){
            return{
                message: ''
            }
        },
        template: `
            {{message}}
            jack<input type="radio" v-model="message" value="jack"/>
            dell<input type="radio" v-model="message" value="dell"/>
            lee<input type="radio" v-model="message" value="lee"/>
        `
    })   
    const vm = app.mount('#root') 
</script>

</html> 

select的双向数据绑定

1.单选select

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>表单中双向绑定指令的使用</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>

</body>

<script>
    const app = Vue.createApp({
        data(){
            return{
                message: ''
            }
        },
        template: `
            {{message}}
            <select v-model="message">
                <option disable value="">请选择内容</option>
                <option value="A">A</option>
                <option value="B">B</option>
                <option value="C">C</option>
            </select>
        `
    })   
    const vm = app.mount('#root') 
</script>

</html> 

2.多选select

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>表单中双向绑定指令的使用</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>

</body>

<script>
    const app = Vue.createApp({
        data(){
            return{
                message: [],
                // options: [
                //     {
                //         text: 'A', value: 'A'
                //     },
                //     {
                //         text: 'B', value: 'B'
                //     },
                //     {
                //         text: 'C', value: 'C'
                //     }
                // ]
                // 数据可以写成对象的形式
                options: [
                    {
                        text: 'A', value: {
                            value: 'A'
                        }
                    },
                    {
                        text: 'B', value: {
                            value: 'B'
                        }
                    },
                    {
                        text: 'C', value: {
                            value: 'C'
                        }
                    }
                ]
            }
        },
        template: `
            {{message}}
            <select v-model="message" multiple>
                <option v-for="item in options" :value="item.value">{{item.text}}</option>
            </select>
        `
    })   
    const vm = app.mount('#root') 
</script>

</html> 

v-model指令修饰符、form表单标签修饰符

lazy修饰符(懒加载修饰符)

input 框 失去焦点 时候会触发 刷新数据的变化 。

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>表单中双向绑定指令的使用</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>

</body>

<script>
    const app = Vue.createApp({
        data(){
            return{
                message: 'hello'
            }
        },
        template: `
            {{message}}
            <input v-model.lazy="message"/>
        `
    })   
    const vm = app.mount('#root') 
</script>

</html> 

2.number修饰符

number修饰符可以做类型的转换 ,如果默认是 string 类型,输入值后 保存到变量中的值是 number 类型 。

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>表单中双向绑定指令的使用</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>

</body>

<script>
    const app = Vue.createApp({
        data(){
            return{
                message: '123'
            }
        },
        template: `
            <div>
                {{ typeof message}}
                <input v-model.number="message" type="number />    
            </div>
        `
    })   
    const vm = app.mount('#root') 
</script>

</html> 

3.trim修饰符

trim修饰符的意思,就是去掉输入框前后的空格

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>表单中双向绑定指令的使用</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>

</body>

<script>
    const app = Vue.createApp({
        data(){
            return{
                message: '123'
            }
        },
        template: `
            <div>
                {{message}}
                <input v-model.trim="message"/>    
            </div>
        `
    })   
    const vm = app.mount('#root') 
</script>

</html> 

三、探索组件的理念

本章中,将会通过对 组件 概念,以及组件之间代码组织,数据传递的内容讲解,帮助大家完整理解组件的设计理念,让大家能够 合理的拆分管理组件 ,写出易于维护的 Vue 代码

1.组件的定义及复用性,局部组件和全局组件

image.png 上图,左面 是 页面 的 整体结构 ,我们可以把它 拆分成 3 个部分 ,头部 、 左侧部分 、 右侧部分 ,每个部分可以再进行拆分 , 这样我们就会发现一个页面可以进行 层层拆分 ,拆成一个像 右侧 一样的 树状结构 ,这就是 拆分组件的思维 。

组件复用性

组件具备共用性(复用性),每个组件的数据是独享的,而不会跟其他组件共享 , 组件在页面多次引用 ,它们的数据不会共享,每个组件都是独立的

全局组件

全局组件名称 :建议用小写横线连接(例如:counter-parent)

我们可以看到下面的代码中, counter-parent组件 与 counter 组件 是兄弟级组件,那为什么在 counter-parent组件 中又可以引用 counter 组件 呢, 这是因为用 app.component 方法定义的组件都会成为全局组件 ,在哪里都可以使用。

缺点 : 如果定义了组件但是页面中未引用该组件的标签元素,这个组件会占用这个app实例化对象的存储空间,一直挂载在这个app实例化对象上 ,所以说它对性能是有一定的损耗的,你不用它,它也在。

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>组件的定义及复用性,局部组件和全局组件</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>
    // 父组件
    const app = Vue.createApp({
        template: `
        <div>
            <counter-parent />
            <counter />    
            <counter />    
            <counter />    
        </div>`
    })   

    // 定义全局组件
    app.component('counter-parent', {
        template: `<counter />`
    })
    
    // 定义全局组件
    app.component('counter', {
        data(){
            return {
                count: 1
            }
        },
        template: `<div @click="count += 1">{{count}}</div>`
    })

    const vm = app.mount('#root') 
</script>

</html> 

局部组件

局部组件名称 :建议用 大驼峰命名(例如:Name)

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>组件的定义及复用性,局部组件和全局组件</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>
    // 1. 创建局部组件(局部组件建议首字母大写)
    const Counter = {
        data(){
            return {
                count: 1
            }
        },
        template: `<div @click="count += 1">{{count}}</div>`
    }

    // 父组件
    const app = Vue.createApp({
        components: { // 2. 引入局部组件
            // Counter: Counter // 组件名:组件值
            // 简写:
            Counter
        },
        template: `
        <div>
            <counter />    
        </div>`
    })   

    const vm = app.mount('#root') 
</script>

</html> 

2.组件间传值及传值校验

在本节将讲述 组件之间的传值的方式 ,以及传过来的 值得校验 。

组件间传值

1.父传子

父组件 中调用 子组件标签 ,通过在标签上添加 属性 的形式 给子组件传值 ,子组件通过 props 来接收 父组件 传入的值

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title> 组件间传值及传值校验</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>
    const app = Vue.createApp({
        template: ` 
        <div>
            <test content="hello world" />
        </div>`
    })   

    // 创建全局组件
    app.component('test', {
        props: ['content'], // 接收父组件传入的值
        template: '<div>{{content}}</div>'
    })

    const vm = app.mount('#root') 
</script>

</html> 

传值校验

有时候 子组件 props 接收父组件传入值 不是想要的 类型 ,所以在这里讲一下 子组件如何校验父组件传过来的值 。

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title> 组件间传值及传值校验</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>
    const app = Vue.createApp({
        data(){
            return { num: 10000 }
        },
        template: ` 
        <div>
            <test :content="num" />
        </div>`
    })   

    // 创建全局组件
    // type: String, Boolean, Array, Object, Function, Symbol 数据的类型
    // required: true / false 必填
    // default: 默认值
    // validator函数:可以做一些逻辑性的校验,例如:传入的值是否小于1000,返回布尔值结果
    app.component('test', {
        props: {
            content: {
                type: Number, // 类型:校验要传入的类型格式Number类型
                default: 789, // 默认值:数组、对象时需要是一个函数返回{}或[]
                validator: function(value) { // value:父组件传递过来的值
                    return value < 1000
                }
            }
        },
        template: '<div>{{content}}</div>'
    })

    const vm = app.mount('#root') 
</script>

</html> 

3.单向数据流的理解

vue中子组件可以使用父组件传递过来的数据,但是绝对不能修改传递过来的数据 。

需求:封装一个 计数器组件 ,根据 父组件传入的数值,在子组件中点击数值后每次自增 + 1 。

上面的需求中, 子组件每次自增 + 1 时,就会 改变父组件的值 ,这样就 违背了vue的单项数据流 , 那么如何解决呢?

解决方案子组件内用一个变量,复制父组件传入的数值副本 ,通过 修改变量副本 达到想要的效果。

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title> 单向数据流的理解</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>
    const app = Vue.createApp({
        data(){
            return {
                num: 1
            }
        },
        template: ` 
        <div>
            <counter :count="num"/>
        </div>`
    })   

    // 创建全局组件
    app.component('counter', {
        props: ['count'],
        data(){
            return {
                myCount: this.count  // 复制一份count
            }
        },
        template: '<div @click="myCount += 1 ">{{myCount}}</div>'
    })

    const vm = app.mount('#root') 
</script>

</html> 

为什么 vue是单项数据流 呢,看下面的代码中, 引入了4次子组件 ,并且4个组件中传入的值 都是 num变量,我们思考一下,如果 其中一个子组件修改了num ,那么其他用到num变量的组件也会被同时改变 ,这样就会出现问题,这就是 单项数据流不允许子组件修改父组件传入的数据,避免数据出现高度耦合 。

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title> 单向数据流的理解</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>
    const app = Vue.createApp({
        data(){
            return {
                num: 1
            }
        },
        template: ` 
        <div>
            <counter :count="num"/>
            <counter :count="num"/>
            <counter :count="num"/>
            <counter :count="num"/>
        </div>`
    })   

    // 创建全局组件
    app.component('counter', {
        props: ['count'],
        data(){
            return {
                myCount: this.count  // 复制一份count
            }
        },
        template: '<div @click="myCount += 1 ">{{myCount}}</div>'
    })

    const vm = app.mount('#root') 
</script>

</html> 

注意

  1. 父组件给子组件传值 ,如果传入的值有很多,可以用 传入对象的形式 。
  2. 如果 自定义的属性名 很长,可以用 中划线分割单词的形式 ,例如: picker-name ,那样的话,在子组件内 props中接收 就要这样写:pickerName

4.Non-Props 属性是什么

  1. inheritAttrs :不接收父组件传入的属性
  2. $attrs: 接收父组件传入的所有属性
  3. $attrs.传递过来的属性: 按需来显示,要显示的属性
  4. 在 生命周期 中使用  $attrs

inheritAttrs

如果 父组件传值,子组件不接收的话,它会把父组件传递过来的内容放在子组件最外层的标签上,变成dom标签的一个属性 ,如下代码:

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Non-Props 属性是什么</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>
    // Non-prop 属性
    const app = Vue.createApp({
        template: ` 
        <div>
            <counter  msg="hello"/>
        </div>`
    })   

    // 子组件未用props接收msg
    app.component('counter', {
        template: '<div>Counter</div>'
    })

    const vm = app.mount('#root') 
</script>

</html> 

子组件在浏览器上就会被渲染 成这样:

<div id="root" data-v-app="">
  <div>
      <!-- 这里会多个msg属性这是因为子组件没有接收父组件传过来的属性 -->
      <div msg="hello">Counter</div>
  </div>
</div>

如果你 不希望渲染时标签上有这样的属性 ,可以 在子组件中添加 inheritAttrs 属性,意思是不继承父组件传过来的props属性 。

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Non-Props 属性是什么</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>
    // Non-prop 属性
    const app = Vue.createApp({
        template: ` 
        <div>
            <counter  msg="hello"/>
        </div>`
    })   

    // 子组件未用props接收msg
    app.component('counter', {
        inheritAttrs: false, // 不继承父组件传过来的props属性
        template: '<div>Counter</div>'
    })

    const vm = app.mount('#root') 
</script>

</html> 

这样写渲染后子组件上就不会有父组件传递过来的属性 了,包括 style,class 也不会被传递过来。

$attrs

父组件传递给子组件的属性(style、class等等)  可以用 指定给子组件的某个元素 ,如下代码:

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Non-Props 属性是什么</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>
    // Non-prop 属性
    const app = Vue.createApp({
        data(){
            return {
                num: 1
            }
        },
        template: ` 
        <div>
            <counter style="color:red" class="div-style" msg="hello"/>
        </div>`
    })   

    app.component('counter', {
        template: `
            <div>Counter</div>
            <div v-bind="$attrs">Counter</div>
            <div>Counter</div>
        `
    })

    const vm = app.mount('#root') 
</script>

</html> 

渲染后的结果如下:

<div id="root" data-v-app="">
  <div>
      <div>Counter</div>
      因为子组件的v-bind="$attrs"是加到第二个元素身上了,所以渲染时都会添加到该标签身上
      <div class="div-style" msg="hello" style="color: red;">Counter</div>
      <div>Counter</div>
  </div>
</div>

$attrs按需显示

如果你 只想在某个标签身上显示某个传递过来的属性 ,你可以这样写:

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Non-Props 属性是什么</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>
    // Non-prop 属性
    const app = Vue.createApp({
        template: ` 
        <div>
            <counter style="color:red" class="div-style" msg="hello"/>
        </div>`
    })   

    app.component('counter', {
        template: `
            <div :style="$attrs.style">Counter</div>
            <div :class="$attrs.class">Counter</div>
            <div :msg="$attrs.msg">Counter</div>
        `
    })

    const vm = app.mount('#root') 
</script>

</html> 

放到浏览器上渲染的结果就是这样:

<div id="root" data-v-app="">
  <div>
      <div style="color: red;">Counter</div>
      <div class="div-style">Counter</div>
      <div msg="hello">Counter</div>
  </div>
</div>

在生命周期中使用$attrs

我们除了可以 在子组件内的标签上使用 attrs,也可以在mounted生命周期中获取到attrs ,也可以在 mounted 生命周期中获取到 attrs 代码如下:

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Non-Props 属性是什么</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>
    // Non-prop 属性
    const app = Vue.createApp({
        template: ` 
        <div>
            <counter style="color:red" class="div-style" msg="hello"/>
        </div>`
    })   

    app.component('counter', {
        mounted(){
            console.log(this.$attrs.style)
        },
        template: `
            <div :style="$attrs.style">Counter</div>
            <div :class="$attrs.class">Counter</div>
            <div :msg="$attrs.msg">Counter</div>
        `
    })

    const vm = app.mount('#root') 
</script>

</html> 

5.父子组件间如何通过事件进行通信

需求 :封装一个 计数器组件 ,父给子传一个数字,子组件中每次点击数字时,数字自增 +1 ,这样就涉及到了需要修改父组件传入的值 ,我们之前讲过 vue 是 单向数据流 ,子组件不可以修改父组件传递的数据 ,之前的 解决办法是复制一份副本修改副本 ,那么我们今天讲一下另外一种解决办法, 我们在子组件通过事件告诉父组件来修改这个值 。

1.子传父

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>父子组件间如何通过事件进行通信</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>
    // 父组件
    const app = Vue.createApp({
        data(){
            return {
                count: 1
            }
        },
        methods: {
            handleAddOne(param){
                this.count += param
            }
        },
        template: `
            <div>
                <counter :count="count" @add-one="handleAddOne" />
            </div>
        `
    })   

    // 子组件
    app.component('counter', {
        props: ['count'],
        methods: {
            handleClick(){
                this.$emit('addOne', 2)
            }
        },
        template: `
            <div @click="handleClick">{{count}}</div>
        `
    })

    const vm = app.mount('#root') 
</script>

</html> 

子组件  $emit('addOne')  时, 事件名可以是 驼峰式命名 ,但是 在标签上使用时应该是中划线方式@add-one="handleAddOne"  。

上面的计算方式是 子组件通过事件来告知父组件,父组件计算后传给子组件 ,也可以在子组件中计算完成后,传入给父组件,父组件再回传回来 ,如下:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>父子组件间如何通过事件进行通信</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>
    // 父组件
    const app = Vue.createApp({
        data(){
            return {
                count: 1
            }
        },
        methods: {
            handleAddOne(param){
                this.count = param
            }
        },
        template: `
            <div>
                <counter :count="count" @add-one="handleAddOne" />
            </div>
        `
    })   

    // 子组件
    app.component('counter', {
        props: ['count'],
        methods: {
            handleClick(){
                this.$emit('addOne', this.count + 2)
            }
        },
        template: `
            <div @click="handleClick">{{count}}</div>
        `
    })

    const vm = app.mount('#root') 
</script>

</html> 

emits

如果一个 子组件向外部触发事件 ,可以写一个 emits 的 对象或数组 ,来 存放$emit定义的事件

emits的作用 : 为了 方便易于维护,写在emits中,一眼就能看到组件中向外传递了哪些事件,很方便, 还可以 校验$emit回传的值的格式

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>父子组件间如何通过事件进行通信</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>
    // 父组件
    const app = Vue.createApp({
        data(){
            return {
                count: 1
            }
        },
        methods: {
            handleAddOne(param){
                this.count = param
            }
        },
        template: `
            <div>
                <counter :count="count" @add-one="handleAddOne" />
            </div>
        `
    })   

    // 子组件
    app.component('counter', {
        props: ['count'],
        // emits: ['addOne'], // 数组形式
        emits: { // 对象形式
            addOne: (count) => { // count参数:this.$emit('addOne', 对应这个count)
                if(count < 0){ // 如果参数大于0,同意事件往外传值
                    return true
                }
                // 如果小于0,不允许往外传,并且给予警告
                return false
            }
        }, // 对象形式
        methods: {  
            handleClick(){
                this.$emit('addOne', this.count + 2)
            }
        },
        template: `
            <div @click="handleClick">{{count}}</div>
        `
    })

    const vm = app.mount('#root') 
</script>

</html> 

6.组件间双向绑定高级内容

v-model的使用同时也可以参考我的另一篇文章中的 v-model和.sync的用法

  1. v-model(修改单个值)v-model绑值,达到子组件可以修改父组件的值的效果
  2. v-model(修改多个值)可以绑定多个v-model值,达到修改多个值

v-model(修改单个值)

上一章讲了讲了如果想做到 子组件想修改父组件的值,子组件要通过$emit触发父组件的自定义方法,然后在父组件中修改值 。其实 v-model 同样可以做到 子组件修改父组件传递的数据,写法如下:

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>组件间双向绑定高级内容</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>
    // 父组件
    const app = Vue.createApp({
        data(){
            return { count: 1 }
        },
        template: `
            <counter v-model="count" />
        `
    })   
 
    // 子组件
    app.component('counter', {
        props: ['modelValue'],
        methods: {  
            handleClick(){
                this.$emit('update:modelValue', this.modelValue + 2)
            }
        },
        template: `
            <div @click="handleClick">{{modelValue}}</div>
        `
    })

    const vm = app.mount('#root') 
</script>

</html> 

上面代码 子组件 中的 modelValue 是 固定写法 ,如果你不想叫做 modelValue ,你可以 自定义名称 ,这样写:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>父子组件间如何通过事件进行通信</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>
    // 父组件
    const app = Vue.createApp({
        data(){
            return { count: 1 }
        },
        template: `
            <counter v-model:app="count" />
        `
    })   
 
    // 子组件
    app.component('counter', {
        props: ['app'],
        methods: {  
            handleClick(){
                this.$emit('update:app', this.app + 2)
            }
        },
        template: `
            <div @click="handleClick">{{app}}</div>
        `
    })

    const vm = app.mount('#root') 
</script>

</html> 

这样写绑定v-model 的地方必须要这样写:  ,拼接成:v-model:组件中props自定义的名称

v-model(修改多个值)

上面的 v-model 例子,看着是简单了很多,但是 只能修改单个属性值 ,那我如果想用 v-model修改多个属性值 该怎么办呢, 代码如下:

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>组件间双向绑定高级内容</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>
    // 父组件
    const app = Vue.createApp({
        data(){
            return { 
                count: 1,
                count1: 1
            }
        },
        template: `
            <counter v-model:count="count" v-model:count1="count1" />
        `
    })   
 
    // 子组件
    app.component('counter', {
        props: ['count', 'count1'],
        methods: {  
            handleClick(){
                this.$emit('update:count', this.count + 2)
            },
            handleClick1(){
                this.$emit('update:count1', this.count1 + 2)
            }
        },
        template: ` 
            <div @click="handleClick">{{count}}</div>
            <div @click="handleClick1">{{count1}}</div>
        `
    })

    const vm = app.mount('#root') 
</script>

</html> 

组件的v-model配合自定义的修饰符使用

在组件的 v-model 上,还可以 自定义修饰符 来使用,只需要 在子组件中判断父组件传入的修饰符是什么,然后执行响应的逻辑来处理v-model绑定的值即可 ,像下面例子中,传入了一个 自定义大写修饰符 ,代码如下:

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>组件间双向绑定高级内容</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>
    // 父组件
    const app = Vue.createApp({
        data(){
            return { 
                count: 'a'
            }
        },
        template: `
            <counter v-model.uppercase="count" />
        `
    })   
 
    // 子组件
    app.component('counter', {
        props: {
            'modelValue': String,
            modelModifiers: { // modelModifiers指传递过来的修饰符
                default: () => ({})
            }
        },
        // mounted(){
        //     // 打印传过来的修饰符
        //     console.log(this.modelModifiers) // uppercase:true
        // },
        methods: {  
            handleClick(){
                let newValue = this.modelValue + 'b'
                if(this.modelModifiers.uppercase){ // 是否使用了uppercase(全部大写)修饰符
                    newValue = newValue.toUpperCase()
                }
                this.$emit('update:modelValue', newValue)
            }
        },
        template: ` 
            <div @click="handleClick">{{modelValue}}</div>
        `
    })

    const vm = app.mount('#root') 
</script>

</html> 

7.使用匿名插槽和具名插槽解决组件内容传递问题

image.png

需求:如图,封装了一个 表单组件 ,但是 按钮不想固定,假如说按钮的位置:我想展示一个div就展示div,想展示button就展示button ,用 slot插槽 就能解决我们的问题。

匿名插槽

匿名插槽 : 就是没有名称的插槽随插随用 ,缺点,只有 使用一个插槽时候可以使用,同时使用多个插槽时候,就不知道插入到哪里了 。

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>使用插槽和具名插槽解决组件内容传递问题</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>
    // slot 插槽
    // slot 中使用的数据,作用域的问题
    // 父模板里调用的数据属性,使用的都是父模板里的数据
    // 子模板里调用的数据属性,使用的都是子模板里的数据
    // 父组件
    const app = Vue.createApp({
        data(){
            return {
                text: '提交'
            }
        },
        template: `
            <myform>
                <div>{{text}}</div>
            </myform>
            <myform> 
                <button>{{text}}</button>
            </myform>
            <myform></myform>
        `
    })   
 
    // 子组件
    app.component('myform', {
        methods: {
            handleClick(){
                alert(123)
            }
        },
        template: ` 
            <div>
                <input />
                <span @click="handleClick">
                    <slot>默认值占位符</slot>
                </span>
            </div>
        `
    })
    const vm = app.mount('#root') 
</script>

</html>  

具名插槽

具名插槽: 明确指定 name ,给对应 name 的 slot 标签中添加对应的内容 。

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>使用插槽和具名插槽解决组件内容传递问题</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>
    // slot 插槽
    // slot 中使用的数据,作用域的问题
    // 父模板里调用的数据属性,使用的都是父模板里的数据
    // 子模板里调用的数据属性,使用的都是子模板里的数据
    // 具名插槽

    // 父组件
    const app = Vue.createApp({
        template: `
        <layout>
            <template v-slot:header>
                <div>header</div>    
            </template>
            <template v-slot:footer>
                <div>footer</div>    
            </template>
        </layout>
        `
    })   
 
    // 子组件
    app.component('layout', {
        template: ` 
            <div>
                <slot name="header"></slot>
                <div>content</div>
                <slot name="footer"></slot>
            </div>
        `
    })
    const vm = app.mount('#root') 
</script>

</html>  

使用 具名插槽 时 , 父组件 中要用 template标签包裹 ,然后写上 v-slot:插槽名称 ,在 template标签包裹 中写要 插入的内容 。

具名插槽语法简写

可以把 template标签 上的 v-slot:header ,简写成  #header 。

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>使用插槽和具名插槽解决组件内容传递问题</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>
    // slot 插槽
    // slot 中使用的数据,作用域的问题
    // 父模板里调用的数据属性,使用的都是父模板里的数据
    // 子模板里调用的数据属性,使用的都是子模板里的数据
    // 具名插槽

    // 父组件
    const app = Vue.createApp({
        template: `
        <layout>
            <template #header>
                <div>header</div>    
            </template>
            <template #footer>
                <div>footer</div>    
            </template>
        </layout>
        `
    })   
 
    // 子组件
    app.component('layout', {
        template: ` 
            <div>
                <slot name="header"></slot>
                <div>content</div>
                <slot name="footer"></slot>
            </div>
        `
    })
    const vm = app.mount('#root') 
</script>

</html>  

注意

  1. 使用 slot插槽 时,必须要用 双标签包裹
  2. slot标签 是没有办法直接 绑定事件 的

8.作用域插槽

需求 : 组件内循环 div 标签,这个标签有可能不仅限于 div ,想通过父组件来决定循环的是什么标签 。

解决办法 :只需要在 子组件slot标签 上 定义属性 传给 父组件 , 父组件 只需要在 调用子组件的标签 上写 v-slot="slotProps"  来 接收数据 ,在 插槽内容 中 slotProps.xxx ,就可以取到传递过来的数据了。

作用域插槽应用场景 : 当子组件的渲染内容,要由父组件决定的时候 。

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>使用插槽和具名插槽解决组件内容传递问题</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>
    // 父组件
    const app = Vue.createApp({
        template: `
        <list v-slot="slotProps">
          <div>
            {{ slotProps.item }}
          </div>
        </list>
        `
    })   
 
    // 子组件 
    app.component('list', {
        data(){
            return {
               list: [1, 2, 3] 
            }
        },
        template: ` 
            <div>
                <slot v-for="item in list" :item="item" />
            </div>
        `
    })
    const vm = app.mount('#root') 
</script>

</html>  

上面的 v-slot="slotProps"  是一个 对象 实际上这里有 简写形式 ,可以写成对象的 对象解构赋值 ,如下:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>使用插槽和具名插槽解决组件内容传递问题</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>
    // 父组件
    const app = Vue.createApp({
        template: `
        <list v-slot="{ item }">
          <div>
            {{ item }}
          </div>
        </list>
        `
    })   
 
    // 子组件 
    app.component('list', {
        data(){
            return {
               list: [1, 2, 3] 
            }
        },
        template: ` 
            <div>
                <slot v-for="item in list" :item="item" />
            </div>
        `
    })
    const vm = app.mount('#root') 
</script>

</html>  

9.动态组件和异步组件

本章讲解 动态组件 与 异步组件 的使用方法

动态组件

动态组件 : 根据数据的变化,结合 component 这和标签,来随时动态切换组件的显示 。

需求 :有两个组件,一个 input框 ,一个 hello world 文字,想实现效果: input框 显示时, hello world 文字就隐藏, hello world 文字显示时, input框 就隐藏 。

如果用 简单写法 就是像下面这样写:

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>动态组件和异步组件</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>
    // 父组件
    const app = Vue.createApp({
        data(){
            return {
                currentItem: 'input-item'
            }
        },
        methods: {
            handleClick(){
                if(this.currentItem === 'input-item'){
                    this.currentItem = 'common-item'
                } else {
                    this.currentItem = 'input-item'
                }
            }
        },
        template: `
            <input-item v-show="currentItem === 'input-item'" />
            <common-item v-show="currentItem === 'common-item'" />
            <button @click="handleClick">切换</button>
        `
    })   
 
    // 子组件 
    app.component('input-item', {
        template: `<input />`
    })
    
    // 子组件 
    app.component('common-item', {
        template: `<div>hello world</div>`
    })

    const vm = app.mount('#root') 
</script>

</html>  

上面的代码看似简单,但是代码量大,下面再看一下用 动态组件 实现的相同效果, 动态组件实际上就是一个 component 标签,通过is属性来决定这个显示的是哪个组件

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>动态组件和异步组件</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>
    // 父组件
    const app = Vue.createApp({
        data(){
            return {
                currentItem: 'input-item'
            }
        },
        methods: {
            handleClick(){
                if(this.currentItem === 'input-item'){
                    this.currentItem = 'common-item'
                } else {
                    this.currentItem = 'input-item'
                }
            }
        },
        template: `
            <component :is="currentItem" />
            <button @click="handleClick">切换</button>
        `
    })   
 
    // 子组件 
    app.component('input-item', {
        template: `<input />`
    })
    
    // 子组件 
    app.component('common-item', {
        template: `<div>hello world</div>`
    })

    const vm = app.mount('#root') 
</script>

</html>  

这样就会有一个问题,在 输入框输入内容后,  点击 切换按钮2次 ,又 回到显示输入框的状态下,发现输入框内输入的内容消失了 , 这时候就需要用到keep-alive标签 来解决这个问题, keep-alive 的意思就是 当这个动态组件第一次渲染时,它会把它里面的一些状态,后面变更的一些情况,都记录下来,当你回头再用这个组件时,会从缓存里,把之前的数据拿过来填充上 ,代码如下:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>动态组件和异步组件</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>
    // 父组件
    const app = Vue.createApp({
        data(){
            return {
                currentItem: 'input-item'
            }
        },
        methods: {
            handleClick(){
                if(this.currentItem === 'input-item'){
                    this.currentItem = 'common-item'
                } else {
                    this.currentItem = 'input-item'
                }
            }
        },
        template: `
            <keep-alive>
                <component :is="currentItem" />
            </keep-alive>
            <button @click="handleClick">切换</button>
        `
    })   
 
    // 子组件 
    app.component('input-item', {
        template: `<input />`
    })
    
    // 子组件 
    app.component('common-item', {
        template: `<div>hello world</div>`
    })

    const vm = app.mount('#root') 
</script>

</html>  

动态组件 与 keep-alive 有时候会一起使用,这点注意一下即可。

异步组件

1.同步组件

首先展示一下 同步组件, 同步组件 就是你 调用这个组件时,这个代码就会立即执行 ,这就叫做 同步组件

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>同步组件</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>
    // 父组件
    const app = Vue.createApp({
        template: `
            <div>
                <common-item />
            </div>
        `
    })   
    
    // 子组件 
    app.component('common-item', {
        template: `<div>hello world</div>`
    })

    const vm = app.mount('#root') 
</script>

</html>  

2.异步组件

异步组件 : 动态加载 一些组件,这样的好处是,我们可以把一些大型的项目,加一些小的 js文件,在需要用到这些小 js文件 的时候,通过 异步组件 来引入这些 js文件 来使用这些 js文件 。

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>同步组件</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>


    // 父组件
    const app = Vue.createApp({
        template: `
            <div>
                <common-item />
                <async-common-item />
            </div>
        `
    })   
    
    // 子组件 
    app.component('common-item', {
        template: `<div>hello world</div>`
    })

    // 异步子组件 
    app.component('async-common-item', Vue.defineAsyncComponent(() => {
        return new Promise((reslve, reject) => {
            setInterval(() => {
                reslve({
                    template: `<div>这是异步组件</div>`
                })
            }, 4000)
        })
    }))

    const vm = app.mount('#root') 
</script>

</html>  

10.基础语法知识点查缺补漏

  1. ref :获取 dom , 调用组件内部方法
  2. provide / inject : 多层组件传值

ref

ref 可以获取 dom 元素 ,获取 dom元素尽量在 mounted 中获取,想获取那个 dom 就在哪个 dom 上添加一个 ref 属性然后 定义一个名称 ,在使用时候直接 this.$refs.定义的属性 即可使用,如下:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ref</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>


    // 父组件
    const app = Vue.createApp({
        data(){
            return {
                count: 1
            }
        },
        mounted(){
            console.log(this.$refs.count)
        },
        template: `
            <div>
                <div ref="count">
                    {{count}}
                </div>
            </div>
        `
    })   

    const vm = app.mount('#root') 
</script>

</html>  

ref 不仅可以获取 dom 元素,还可以用来 调用组件内部方法 ,如下:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ref</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>


    // 父组件
    const app = Vue.createApp({
        data(){
            return {
                count: 1
            }
        },
        mounted(){
            this.$refs.common.sayHello()
        },
        template: `
            <div>
                <common-item ref="common" />
            </div>
        `
    })   

    app.component('common-item', {
        methods: {
            sayHello(){
                alert('hello')
            },
        },
        template: `<div>hello world</div>`
    })

    const vm = app.mount('#root') 
</script>

</html>  

provide / inject

如果有这样一个结构, 父亲组件 > 儿子组件 > 孙子组件 , 父组件调用子组件,子组件调用孙子组件 ,如果 父组件 想给 孙子组件传一个值就需要一层一层的传递这个参数,也就是 跨层传参 ,我们通常会这样写:

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>provide / inject</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>
    // 父亲组件
    const app = Vue.createApp({
        data(){
            return { count: 1 }
        },
        template: `
            <div>
                <child :count="count" />
            </div>
        `
    })   

    // 儿子组件
    app.component('child', {
        props: ['count'],
        template: `<child-child  :count="count" />`
    })

    // 孙子组件
    app.component('child-child', {
        props: ['count'],
        template: `<div>{{count}}</div>`
    })

    const vm = app.mount('#root') 
</script>

</html>  

这种需要 层层的传递 ,那假如还有 爷爷组件 ,甚至 太爷爷组件 ,难道要 跨域 4层 / 5层的组件 传递来传递这个值吗,这时候我们可以使用 vue 提供的 provide / inject

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>provide / inject</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div>
</body>

<script>
    // 父亲组件
    const app = Vue.createApp({
        data(){
            return {
                count: 3
            }
        },
        // provide: { // 1. 向下传递值(给孙子传值)
        //     count: 2
        // },
        // 如果provide的值想传data中的变量,必须用函数的方式,像下面这样
        provide(){ // 1. 向下传递值(给孙子传值)
            return {
                count: this.count
            }
        },
        template: `
            <div>
                <child />
                <button @click="count += 1">Add</button>
            </div>
        `
    })   

    // 儿子组件
    app.component('child', {
        template: `<child-child />`
    })

    // 孙子组件
    app.component('child-child', {
        inject: ['count'], // 2. 接收上层组件传过来的值(接收爷爷传过来的值)
        template: `<div>{{count}}</div>`
    })

    const vm = app.mount('#root') 
</script>

</html>  

其实 provide / inject 目前还有一个问题,他的 数据是一次性传递过去的,数据后续发生变化,孙子组件的数据不会更新 ,那么在后续讲解 vue3.0 中,我们会讲解如何解决这个问题。