VUE3教程

104 阅读13分钟

安装

npm  6.x
npm  init  vite@latest  project-name  --template  vue
npm  7+,需要加上额外的双短横线
npm  init  vite@latest  project-name  --  --template  vue
cd  project-name
npm  install
npm  run  dev

介绍

声明式渲染:

指令带有前缀  v-以表示它们是  Vue  提供的特殊  attribute:v-on  
指令添加一个事件监听器(<button  v-on:click="reverseMessage">反转message)

应用&组件实例

组件实例的所有  property,无论如何定义,都可以在组件的模板中访问。

模板语法

插值

1. 文本:Mastuch {{msg}}双大括号标签会被替换为相应组件实例中 msg 属性的值。
2. 原始html:v-html="rawHtml"(动态渲染任意的  HTML  是非常危险的)
3. 使用 v-bind 指令:

指令

  • v-bind <a v-bind:href="url">  ...  </a>在这里 href 是参数,告知  v-bind  指令将该元素的 href attribute 与表达式 url 的值绑定。
  • v-on指令,它用于监听 DOM 事件
  • 语法糖——v-bind : ; v-on @
  • 动态参数:也可以在指令参数中使用  JavaScript  表达式,方法是用方括号括起来:<a v-bind:[attributeName]="url">  ...  </a>。这里的  attributeName  会被作为一个  JavaScript  表达式进行动态求值,求得的值将会作为最终的参数来使用。
    注意:
    property是DOM中的属性,是JavaScript里的对象;
    attribute是HTML标签上的特性,它的值只能够是字符串;

Data Property 和方法

Data Property

vue中组件是用来复用的,为了防止data复用,将其定义为函数。
vue组件中的data数据都应该是相互隔离,互不影响的,组件每复用一次,data数据就应该被复制一次,之后,当某一处复用的地方组件内data数据被改变时,其他复用地方组件的data数据不受影响,就需要通过data函数返回一个对象作为组件的状态。

data: function() {
    return {
      map:{},
      mousePosition: "",
      showLocation: false
    }
  },

方法

const  app  =  Vue.createApp({
    data()  {
        return  {  count:  4  }
    },
    methods:  {
        increment()  {
            //  `this`  指向该组件实例
            this.count++
        }
    }
})

计算属性和侦听器

计算属性

Vue.createApp({
    data()  {
        return  {
            author:  {
                name:  'John  Doe',
                books:  [
                    'Vue  2  -  Advanced  Guide',
                    'Vue  3  -  Basic  Guide',
                    'Vue  4  -  The  Mystery'
                ]
            }
        }
    },

   computed:  {
        //  计算属性的  getter
        publishedBooksMessage()  {
            //  `this`  指向  vm  实例
            return  this.author.books.length  >  0  ?  'Yes'  :  'No'
        }
    }
}).mount('#computed-basics')

计算属性将基于它们的响应依赖关系缓存。 计算属性只会在相关响应式依赖发生改变时重新求值。这就意味着只要  author.books  还没有发生改变,多次访问  publishedBookMessage  时计算属性会立即返回之前的计算结果,而不必再次执行函数。
计算属性默认只有  getter,不过在需要时你也可以提供一个 setter

computed:  {
    fullName:  {
        //  getter
        get()  {
            return  this.firstName  +  '  '  +  this.lastName
        },
        //  setter
        set(newValue)  {
            const  names  =  newValue.split('  ')
            this.firstName  =  names[0]
            this.lastName  =  names[names.length  -  1]
        }
    }
}

侦听器

当需要在数据变化时执行异步或开销较大的操作时,使用侦听器。

<div  id="demo">{{  fullName  }}</div>
//侦听器
const  vm  =  Vue.createApp({
    data()  {
        return  {
            firstName:  'Foo',
            lastName:  'Bar',
            fullName:  'Foo  Bar'
        }
    },
    watch:  {
        firstName(valNew,valOld)  {
            this.fullName  =  val  +  '  '  +  this.lastName
        },
        lastName(valNew,valOld)  {
            this.fullName  =  this.firstName  +  '  '  +  val
        }
    }
}).mount('#demo')

//计算属性
const  vm  =  Vue.createApp({
    data()  {
        return  {
            firstName:  'Foo',
            lastName:  'Bar'
        }
    },
    computed:  {
        fullName()  {
            return  this.firstName  +  '  '  +  this.lastName
        }
    }
}).mount('#demo')

深度监听: deep:ture,    //  表示深度监听  侦听器会一层一层向下遍历,给每一个属性都加上侦听器; 字符串监听:

watch:  {
    'obj.a':  {
        handler(newName,  oldName)  {
            console.log('obj.a  changed');
        },
        immediate:  true,
        deep:  true
    }
}

VUE3 计算属性和侦听器

侦听器

watch函数有3个参数,一个想要侦听的响应式引用或  getter  函数,  一个回调,  可选的配置选项。
watch 的第一个参数可以是不同形式的“数据源”:它可以是一个 ref (包括计算属性)、一个响应式对象、一个 getter 函数、或多个数据源组成的数组:

import { ref, watch } from 'vue'
const x = ref(0)
const y = ref(0)

// 单个 ref
watch(x, (newX) => {
  console.log(`x is ${newX}`)
})

// getter 函数
watch(
  () => x.value + y.value,
  (sum) => {
    console.log(`sum of x + y is: ${sum}`)
  }
)

// 多个来源组成的数组
watch([x, () => y.value], ([newX, newY]) => {
  console.log(`x is ${newX} and y is ${newY}`)
})

深层侦听器

直接给 watch() 传入一个响应式对象,会隐式地创建一个深层侦听器——该回调函数在所有嵌套的变更时都会被触发

const obj = reactive({ count: 0 })

watch(obj, (newValue, oldValue) => {
  // 在嵌套的属性变更时触发
  // 注意:`newValue` 此处和 `oldValue` 是相等的
  // 因为它们是同一个对象!
})

obj.count++

即时回调的侦听器

watch 默认是懒执行的:仅当数据源变化时,才会执行回调。但在某些场景中,我们希望在创建侦听器时,立即执行一遍回调。举例来说,我们想请求一些初始数据,然后在相关状态更改时重新请求数据

我们可以通过传入 immediate: true 选项来强制侦听器的回调立即执行:

watch(source, (newValue, oldValue) => {
  // 立即执行,且当 `source` 改变时再次执行
}, { immediate: true })

Class与Style绑定

绑定HTML Class

对象语法

<div
    class="static"
    :class="{  active:  isActive,  'text-danger':  hasError  }"
></div>

active 是否存在取决于数据属性 isActive 的真假值。 绑定一个返回对象的计算属性。这是一个常用且强大的模式。

<div  :class="classObject"></div>
data()  {
    return  {
        isActive:  true,
        error:  null
    }
},

computed:  {
    classObject()  {
        return  {
            active:  this.isActive  &&  !this.error,
            'text-danger':  this.error  &&  this.error.type  ===  'fatal'
        }
    }
}

数组语法
数组可与对象结合使用

const activeClass = ref('active')
const errorClass = ref('text-danger')
----------------------------------------------
<div :class="[activeClass, errorClass]"></div>

绑定内联样式

const activeColor = ref('red')
const fontSize = ref(30)
-----------------------------------
<div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
const styleObject = reactive({
  color: 'red',
  fontSize: '13px'
})
-------------------------------------
<div :style="styleObject"></div>

同样的,如果样式对象需要更复杂的逻辑,也可以使用返回样式对象的计算属性。
我们还可以给 :style 绑定一个包含多个样式对象的数组。这些对象会被合并后渲染到同一元素上:

<div :style="[baseStyles, overridingStyles]"></div>

条件渲染

v-if

v-if 指令用于条件性地渲染一块内容。这块内容只会在指令的表达式返回真值时才被渲染。

<h1 v-if="awesome">Vue is awesome!</h1>

v-else

你也可以使用 v-else 为 v-if 添加一个“else 区块”。

<button @click="awesome = !awesome">Toggle</button>

<h1 v-if="awesome">Vue is awesome!</h1>
<h1 v-else>Oh no 😢</h1>

注意:v-else  元素必须紧跟在带  v-if  或者  v-else-if  的元素的后面,否则它将不会被识别。

v-else-if

v-else-if 提供的是相应于 v-if 的“else if 区块”。它可以连续多次重复使用:

<div v-if="type === 'A'">
  A
</div>
<div v-else-if="type === 'B'">
  B
</div>
<div v-else-if="type === 'C'">
  C
</div>
<div v-else>
  Not A/B/C
</div>

v-show

<h1 v-show="ok">Hello!</h1>

v-show 会在 DOM 渲染中保留该元素;v-show 仅切换了该元素上名为 display 的 CSS 属性。
v-show 不支持在 <template> 元素上使用,也不能和 v-else 搭配使用。

v-if vs. v-show

v-if 是“真实的”按条件渲染,因为它确保了在切换时,条件区块内的事件监听器和子组件都会被销毁与重建。

v-if 也是惰性的:如果在初次渲染时条件值为 false,则不会做任何事。条件区块只有当条件首次变为 true 时才被渲染。

相比之下,v-show 简单许多,元素无论初始条件如何,始终会被渲染,只有 CSS display 属性会被切换。

总的来说,v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销。因此,如果需要频繁切换,则使用 v-show 较好;如果在运行时绑定条件很少改变,则 v-if 会更合适。

列表渲染

v-for

const items = ref([{ message: 'Foo' }, { message: 'Bar' }])
------------------------------------------------------------
<li v-for="item in items">
  {{ item.message }}
</li>

在 v-for 块中可以完整地访问父作用域内的属性和变量。v-for 也支持使用可选的第二个参数表示当前项的位置索引。

const parentMessage = ref('Parent')
const items = ref([{ message: 'Foo' }, { message: 'Bar' }])
----------------------------------------------------------------
<li v-for="(item, index) in items">
  {{ parentMessage }} - {{ index }} - {{ item.message }}
</li>

在定义 v-for 的变量别名时使用解构,和解构函数参数类似:

<li v-for="{ message } in items">
  {{ message }}
</li>

<!-- 有 index 索引时 -->
<li v-for="({ message }, index) in items">
  {{ message }} {{ index }}
</li>

不必使用item.message来获取message

v-for与 v-if

同时使用 v-if 和 v-for 是不推荐的,因为这样二者的优先级不明显。
在外新包装一层 <template> 再在其上使用 v-for 可以解决这个问题 (这也更加明显易读):

<template v-for="todo in todos">
  <li v-if="!todo.isComplete">
    {{ todo.name }}
  </li>
</template>

通过 key 管理状态

vue中v-for要加key且index为何不推荐作为key

list = [
    {
        id: 1,
        num: 1
    },
    {
        id: 4,
        num: '新增加的数据4'
    },
    {
        id: 2,
        num: 2
    },
    {
        id: 3,
        num: 3
    }
];
-----------------------------------------
<ul><li v-for="(item,i) in list">
        <input type="checkbox">{{item.num}}
    </li>
</ul>

v-for默认使用就地复用策略,列表数据修改的时候,会根据key值去判断某个值是否修改,如果修改,则重新渲染这一项,否则复用之前的元素,如果不绑定key,每次修改某一条数据,都会重新渲染所有数据,会导致大量内存的浪费。如果绑定了key,每次修改某一条数据的时候,就只会重新渲染改条数据的变化,节省了大量的内存。

变更方法

Vue 能够侦听响应式数组的变更方法(改变原数组),并在它们被调用时触发相关的更新。这些变更方法包括:

  • push()
  • pop()
  • shift()
  • unshift()
  • splice()
  • sort()
  • reverse()

注意:vue3可以通过索引值响应数组修改。Vue2不可以

事件处理

监听事件

我们可以使用 v-on 指令 (简写为 @) 来监听 DOM 事件,并在事件触发时执行对应的 JavaScript。用法:v-on:click="handler" 或 @click="handler"

事件处理器 (handler) 的值可以是:

  1. 内联事件处理器:事件被触发时执行的内联 JavaScript 语句 (与 onclick 类似)。
  2. 方法事件处理器:一个指向组件上定义的方法的属性名或是路径。

内联事件处理器

const count = ref(0)
--------------------------------
<button @click="count++">Add 1</button>
<p>Count is: {{ count }}</p>

方法事件处理器

const name = ref('Vue.js')

function greet(event) {
  alert(`Hello ${name.value}!`)
  // `event` 是 DOM 原生事件
  if (event) {
    alert(event.target.tagName)
  }
}
------------------------------
<!-- `greet` 是上面定义过的方法名 -->
<button @click="greet">Greet</button>

在内联处理器中调用方法

function say(message) {
  alert(message)
}
-----------------------
<button @click="say('hello')">Say hello</button>
<button @click="say('bye')">Say bye</button>

在内联事件处理器中访问事件参数

<!-- 使用特殊的 $event 变量 -->
<button @click="warn('Form cannot be submitted yet.', $event)">
  Submit
</button>

<!-- 使用内联箭头函数 -->
<button @click="(event) => warn('Form cannot be submitted yet.', event)">
  Submit
</button>
----------------------------------------------------------------------------
function warn(message, event) {
  // 这里可以访问原生事件
  if (event) {
    event.preventDefault()
  }
  alert(message)
}

多事件处理器

<button  @click="one($event),  two($event)">
     Submit
</button>
------------------------------------------------
methods:  {
    one(event)  {
        //  第一个事件处理器逻辑...
    },
    two(event)  {
      //  第二个事件处理器逻辑...
    }
}

一个事件绑定多个函数,注意不要省略括号。

事件修饰符

  • .stop  阻止单击事件继续冒泡
  • .prevent  阻止默认行为
  • .capture 使用事件的捕获模式
  • .self 只有event.target是当前操作的元素时才触发事件
  • .once  只触发一次 · .passive 永远不会调用 preventDefault()
<!-- 单击事件将停止传递 -->
<a @click.stop="doThis"></a>

<!-- 提交事件将不再重新加载页面 -->
<form @submit.prevent="onSubmit"></form>

<!-- 修饰语可以使用链式书写 -->
<a @click.stop.prevent="doThat"></a>

<!-- 也可以只有修饰符 -->
<form @submit.prevent></form>

<!-- 仅当 event.target 是元素本身时才会触发事件处理器 -->
<!-- 例如:事件处理器不来自子元素 -->
<div @click.self="doThat">...</div>

按键修饰符

键盘事件:@keydown(键盘按下时触发),@keypress(键盘按住时触发),@keyup(键盘弹起)

<input @keyup.page-down="onPageDown" />

使用 [KeyboardEvent.key]暴露的按键名称作为修饰符,但需要转为 kebab-case 形式。
仅会在 $event.key 为 'PageDown' 时调用事件处理。

按键别名

Vue 为一些常用的按键提供了别名:

  • .enter
  • .tab
  • .delete (捕获“Delete”和“Backspace”两个按键)
  • .esc
  • .space
  • .up
  • .down
  • .left
  • .right

系统按键修饰符

你可以使用以下系统按键修饰符来触发鼠标或键盘事件监听器,只有当按键被按下时才会触发。

  • .ctrl
  • .alt
  • .shift
  • .meta
<!-- Alt + Enter -->
<input @keyup.alt.enter="clear" />

<!-- Ctrl + 点击 -->
<div @click.ctrl="doSomething">Do something</div>

请注意,系统按键修饰符和常规按键不同。与 keyup 事件一起使用时,该按键必须在事件发出时处于按下状态。换句话说,keyup.ctrl 只会在你仍然按住 ctrl 但松开了另一个键时被触发。若你单独松开 ctrl 键将不会触发。

表单输入绑定

在前端处理表单时,我们常常需要将表单输入框的内容同步给 JavaScript 中相应的变量。手动连接值绑定和更改事件监听器可能会很麻烦:

<input
  :value="text"
  @input="event => text = event.target.value">

v-model 指令帮我们简化了这一步骤:

<input v-model="text">

v-model 还可以用于各种不同类型的输入,<textarea><select> 元素。它会根据所使用的元素自动使用对应的 DOM 属性和事件组合:

  • 文本类型的 <input> 和 <textarea> 元素会绑定 value property 并侦听 input 事件;
  • <input type="checkbox"> 和 <input type="radio"> 会绑定 checked property 并侦听 change 事件;
  • <select> 会绑定 value property 并侦听 change 事件。

inpput

<p>Message is: {{ message }}</p>
<input v-model="message" placeholder="edit me" />

屏幕截图 2023-08-16 113556.png

<input type="checkbox" id="checkbox" v-model="checked" />
<label for="checkbox">{{ checked }}</label>

屏幕截图 2023-08-16 113556.png

<div>Picked: {{ picked }}</div>

<input type="radio" id="one" value="One" v-model="picked" />
<label for="one">One</label>

<input type="radio" id="two" value="Two" v-model="picked" />
<label for="two">Two</label>

屏幕截图 2023-08-16 113556.png

testarea

<span>Multiline message is:</span>
<p style="white-space: pre-line;">{{ message }}</p>
<textarea v-model="message" placeholder="add multiple lines"></textarea>

值绑定

???????表单输入绑定 | Vue.js (vuejs.org)

修饰符

  • .lazy  当输入框失去焦点,再去同步输入数据。
  • .number  将输入框自动转为数字类型
  • .trim  自动去除首尾空白字符
<!-- 在 "change" 事件后同步更新而不是 "input" -->
<input v-model.lazy="msg" />

组件基础

定义一个组件

<script setup>
import { ref } from 'vue'

const count = ref(0)
</script>

<template>
  <button @click="count++">You clicked me {{ count }} times.</button>
</template>

使用组件

<script setup>
import ButtonCounter from './ButtonCounter.vue'
</script>

<template>
  <h1>Here is a child component!</h1>
  <ButtonCounter />
</template>

传递 props

如果我们正在构建一个博客,我们可能需要一个表示博客文章的组件。我们希望所有的博客文章分享相同的视觉布局,但有不同的内容。要实现这样的效果自然必须向组件中传递数据,例如每篇文章标题和内容,这就会使用到 props。

<BlogPost title="My journey with Vue" />
<BlogPost title="Blogging with Vue" />
<BlogPost title="Why Vue is so fun" />
---------------------------------------------
<!-- BlogPost.vue -->
<script setup>
defineProps(['title'])
</script>

<template>
  <h4>{{ title }}</h4>
</template>
//`defineProps` 是一个仅 `<script setup>` 中可用的编译宏命令,并不需要显式地导入。
//声明的 props 会自动暴露给模板。`defineProps` 会返回一个对象,其中包含了可以传递给组件的所有 props:
-----------------------------------------------
//如果你没有使用 `<script setup>`,props 必须以 `props` 选项的方式声明,
//props 对象会作为 `setup()` 函数的第一个参数被传入:
export default {
  props: ['title'],
  setup(props) {
    console.log(props.title)
  }
}

在实际应用中,我们可能在父组件中会有如下的一个博客文章数组:

const posts = ref([
  { id: 1, title: 'My journey with Vue' },
  { id: 2, title: 'Blogging with Vue' },
  { id: 3, title: 'Why Vue is so fun' }
])
-------------------------------------------
<BlogPost
  v-for="post in posts"
  :key="post.id"
  :title="post.title"
 />

Props 声明

<script setup>
const props = defineProps(['foo'])

console.log(props.foo)
</script>

除了使用字符串数组来声明 prop 外,还可以使用对象的形式:

// 使用 <script setup>
defineProps({
  title: String,
  likes: Number
})

传递 prop 的细节

如果一个 prop 的名字很长,应使用 camelCase 形式,因为它们是合法的 JavaScript 标识符,可以直接在模板的表达式中使用,也可以避免在作为属性 key 名时必须加上引号。

defineProps({
  greetingMessage: String
})
------------------------
<span>{{ greetingMessage }}</span>

静态 vs. 动态 Prop

至此,你已经见过了很多像这样的静态值形式的 props:

<BlogPost title="My journey with Vue" />

相应地,还有使用 v-bind 或缩写 : 来进行动态绑定的 props:

<!-- 根据一个变量的值动态传入 -->
<BlogPost :title="post.title" />

<!-- 根据一个更复杂表达式的值动态传入 -->
<BlogPost :title="post.title + ' by ' + post.author.name" />

传递不同的值类型

在上述的两个例子中,我们只传入了字符串值,但实际上任何类型的值都可以作为 props 的值被传递。

Number

<!-- 虽然 `42` 是个常量,我们还是需要使用 v-bind -->
<!-- 因为这是一个 JavaScript 表达式而不是一个字符串 -->
<BlogPost :likes="42" />

<!-- 根据一个变量的值动态传入 -->
<BlogPost :likes="post.likes" />

Boolean

<!-- 仅写上 prop 但不传值,会隐式转换为 `true` -->
<BlogPost is-published />

<!-- 虽然 `false` 是静态的值,我们还是需要使用 v-bind -->
<!-- 因为这是一个 JavaScript 表达式而不是一个字符串 -->
<BlogPost :is-published="false" />

<!-- 根据一个变量的值动态传入 -->
<BlogPost :is-published="post.isPublished" />

Array

<!-- 虽然这个数组是个常量,我们还是需要使用 v-bind -->
<!-- 因为这是一个 JavaScript 表达式而不是一个字符串 -->
<BlogPost :comment-ids="[234, 266, 273]" />

<!-- 根据一个变量的值动态传入 -->
<BlogPost :comment-ids="post.commentIds" />

Object

<!-- 虽然这个对象字面量是个常量,我们还是需要使用 v-bind -->
<!-- 因为这是一个 JavaScript 表达式而不是一个字符串 -->
<BlogPost
  :author="{
    name: 'Veronica',
    company: 'Veridian Dynamics'
  }"
 />

<!-- 根据一个变量的值动态传入 -->
<BlogPost :author="post.author" />

使用一个对象绑定多个 prop

如果你想要将一个对象的所有属性都当作 props 传入,你可以使用[没有参数的 v-bind] ,即只使用 v-bind 而非 :prop-name

const post = {
  id: 1,
  title: 'My Journey with Vue'
}
-----------------------
<BlogPost v-bind="post" />
----------------------等价于:
<BlogPost :id="post.id" :title="post.title" />

单向数据流

所有的 props 都遵循着单向绑定原则,props 因父组件的更新而变化,自然地将新的状态向下流往子组件,而不会逆向传递。这避免了子组件意外修改父组件的状态的情况,不然应用的数据流将很容易变得混乱而难以理解。 另外,每次父组件更新后,所有的子组件中的 props 都会被更新到最新值,这意味着你不应该在子组件中去更改一个 prop。若你这么做了,Vue 会在控制台上向你抛出警告:

const props = defineProps(['foo'])

// ❌ 警告!prop 是只读的!
props.foo = 'bar'

导致你想要更改一个 prop 的需求通常来源于以下两种场景:

  1. prop 被用于传入初始值;而子组件想在之后将其作为一个局部数据属性。在这种情况下,最好是新定义一个局部数据属性,从 props 上获取初始值即可:
    const props = defineProps(['initialCounter'])
    
    // 计数器只是将 props.initialCounter 作为初始值
    // 像下面这样做就使 prop 和后续更新无关了
    const counter = ref(props.initialCounter)    ```
    
    
  2. 需要对传入的 prop 值做进一步的转换。在这种情况中,最好是基于该 prop 值定义一个计算属性:
    const props = defineProps(['size'])
    
    // 该 prop 变更时计算属性也会自动更新
    const normalizedSize = computed(() => props.size.trim().toLowerCase())
    

更改对象 / 数组类型的 props

当对象或数组作为 props 被传入时,虽然子组件无法更改 props 绑定,但仍然可以更改对象或数组内部的值。这是因为 JavaScript 的对象和数组是按引用传递,而对 Vue 来说,禁止这样的改动,虽然可能生效,但有很大的性能损耗,比较得不偿失。

这种更改的主要缺陷是它允许了子组件以某种不明显的方式影响父组件的状态,可能会使数据流在将来变得更难以理解。在最佳实践中,你应该尽可能避免这样的更改,除非父子组件在设计上本来就需要紧密耦合。在大多数场景下,子组件应该抛出一个事件来通知父组件做出改变。

prop 校验

defineProps({
  // 基础类型检查
  // (给出 `null` 和 `undefined` 值则会跳过任何类型检查)
  propA: Number,
  // 多种可能的类型
  propB: [String, Number],
  // 必传,且为 String 类型
  propC: {
    type: String,
    required: true
  },
  // Number 类型的默认值
  propD: {
    type: Number,
    default: 100
  },
  // 对象类型的默认值
  propE: {
    type: Object,
    // 对象或数组的默认值
    // 必须从一个工厂函数返回。
    // 该函数接收组件所接收到的原始 prop 作为参数。
    default(rawProps) {
      return { message: 'hello' }
    }
  },
  // 自定义类型校验函数
  propF: {
    validator(value) {
      // The value must match one of these strings
      return ['success', 'warning', 'danger'].includes(value)
    }
  },
  // 函数类型的默认值
  propG: {
    type: Function,
    // 不像对象或数组的默认,这不是一个
    // 工厂函数。这会是一个用来作为默认值的函数
    default() {
      return 'Default function'
    }
  }
})

defineProps() 宏中的参数不可以访问 <script setup> 中定义的其他变量,因为在编译时整个表达式都会被移到外部的函数中。

一些补充细节:

  • 所有 prop 默认都是可选的,除非声明了 required: true
  • 除 Boolean 外的未传递的可选 prop 将会有一个默认值 undefined
  • Boolean 类型的未传递 prop 将被转换为 false。这可以通过为它设置 default 来更改——例如:设置为 default: undefined 将与非布尔类型的 prop 的行为保持一致。
  • 如果声明了 default 值,那么在 prop 的值被解析为 undefined 时,无论 prop 是未被传递还是显式指明的 undefined,都会改为 default 值。

当 prop 的校验失败后,Vue 会抛出一个控制台警告 (在开发模式下)。

运行时类型检查

校验选项中的 type 可以是下列这些原生构造函数:

  • String
  • Number
  • Boolean
  • Array
  • Object
  • Date
  • Function
  • Symbol 另外,type 也可以是自定义的类或构造函数,Vue 将会通过 instanceof 来检查类型是否匹配。例如下面这个类:
class Person {
  constructor(firstName, lastName) {
    this.firstName = firstName
    this.lastName = lastName
  }
}

你可以将其作为一个 prop 的类型:

defineProps({
  author: Person
})

Boolean 类型转换

???????????????????????

监听事件

让我们继续关注我们的 <BlogPost> 组件。我们会发现有时候它需要与父组件进行交互。例如,要在此处实现无障碍访问的需求,将博客文章的文字能够放大,而页面的其余部分仍使用默认字号。

  • 在父组件中,我们可以添加一个 postFontSize ref
  • 给 <BlogPost> 组件添加一个按钮:
  • 父组件可以通过 v-on 或 @ 来选择性地监听子组件上抛的事件,就像监听原生 DOM 事件那样:
  • 子组件可以通过调用内置的  $emit 方法,通过传入事件名称来抛出一个事件:
//父组件:
const posts = ref([
  /* ... */
])

const postFontSize = ref(1)
---------------------------
<div :style="{ fontSize: postFontSize + 'em' }">
  <BlogPost
    v-for="post in posts"
    :key="post.id"
    :title="post.title"
    @enlarge-text="postFontSize += 0.1"
   />
</div>
-----------------------------

//子组件:
<!-- BlogPost.vue -->
<script setup>
defineProps(['title'])
defineEmits(['enlarge-text'])
</script>

<template>
  <div class="blog-post">
    <h4>{{ title }}</h4>
    <button @click="$emit('enlarge-text')">Enlarge text</button>
  </div>
</template>

通过插槽来分配内容

插槽:为子组件传递一些模板片段,让子组件在它们的组件中渲染这些片段。 这里有一个 <FancyButton> 组件,可以像这样使用:

<FancyButton>
  Click me! <!-- 插槽内容 -->
</FancyButton>

而 <FancyButton> 的模板是这样的:

<button class="fancy-btn">
  <slot></slot> <!-- 插槽出口 -->
</button>

<slot> 元素是一个插槽出口 (slot outlet),标示了父元素提供的插槽内容 (slot content) 将在哪里被渲染。

slots.dbdaf1e8.png 最终渲染出的 DOM 是这样:

<button class="fancy-btn">Click me!</button>

插槽内容可以是任意合法的模板内容,不局限于文本。例如我们可以传入多个元素,甚至是组件:

<FancyButton>
  <span style="color:red">Click me!</span>
  <AwesomeIcon name="plus" />
</FancyButton>

渲染作用域

插槽内容可以访问到父组件的数据作用域,因为插槽内容本身是在父组件模板中定义的。

<span>{{ message }}</span>
<FancyButton>{{ message }}</FancyButton>

插槽内容无法访问子组件的数据。
父组件模板中的表达式只能访问父组件的作用域;子组件模板中的表达式只能访问子组件的作用域。

默认内容

在外部没有提供任何内容的情况下,可以为插槽指定默认内容。比如有这样一个 <SubmitButton> 组件:

<button type="submit">
  <slot>
    Submit <!-- 默认内容 -->
  </slot>
</button>

当我们在父组件中使用 <SubmitButton> 且没有提供任何插槽内容时:

<SubmitButton />

“Submit”将会被作为默认内容渲染:

<button type="submit">Submit</button>

但如果我们提供了插槽内容:

<SubmitButton>Save</SubmitButton>

那么被显式提供的内容会取代默认内容:

<button type="submit">Save</button>

具名插槽

子组件:

<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>

父组件:

<BaseLayout>
  <template v-slot:header>
    <!-- header 插槽的内容放这里 -->
  </template>
</BaseLayout>

v-slot 有对应的简写 #,因此 <template v-slot:header> 可以简写为 <template #header>。其意思就是“将这部分模板片段传入子组件的 header 插槽中”。

动态插槽名

动态指令参数在 v-slot 上也是有效的,即可以定义下面这样的动态插槽名:

<base-layout>
  <template v-slot:[dynamicSlotName]>
    ...
  </template>

  <!-- 缩写为 -->
  <template #[dynamicSlotName]>
    ...
  </template>
</base-layout>

作用域插槽

插槽的内容无法访问到子组件的状态。

某些场景下插槽的内容可能想要同时使用父组件域内和子组件域内的数据。

可以像对组件传递 props 那样,向一个插槽的出口上(子组件)传递 attributes:

<!-- <MyComponent> 的模板 -->
<div>
  <slot :text="greetingMessage" :count="1"></slot>
</div>

通过子组件标签上的 v-slot 指令,直接接收到了一个插槽 props 对象:
作用域插槽:v-slot="slotProps"-----------具名插槽为:v-slot:header

<MyComponent v-slot="slotProps">
  {{ slotProps.text }} {{ slotProps.count }}
</MyComponent>

v-slot="slotProps" 可以类比这里的函数签名,和函数的参数类似,我们也可以在 v-slot 中使用解构:

<MyComponent v-slot="{ text, count }">
  {{ text }} {{ count }}
</MyComponent>

具名作用域插槽

具名作用域插槽的工作方式也是类似的,插槽 props 可以作为 v-slot 指令的值被访问到:v-slot:name="slotProps"。当使用缩写时是这样:

<MyComponent>
  <template #header="headerProps">
    {{ headerProps }}
  </template>

  <template #default="defaultProps">
    {{ defaultProps }}
  </template>

  <template #footer="footerProps">
    {{ footerProps }}
  </template>
</MyComponent>

向具名插槽中传入 props:

<slot name="header" message="hello"></slot>

注意插槽上的 name 是一个 Vue 特别保留的 attribute,不会作为 props 传递给插槽。因此最终 headerProps 的结果是 { message: 'hello' }

如果你同时使用了具名插槽与默认插槽,则需要为默认插槽使用显式的 <template> 标签。

<!-- 该模板无法编译 -->
<template>
  <MyComponent v-slot="{ message }">
    <p>{{ message }}</p>
    <template #footer>
      <!-- message 属于默认插槽,此处不可用 -->
      <p>{{ message }}</p>
    </template>
  </MyComponent>
</template>

为默认插槽使用显式的 <template> 标签有助于更清晰地指出 message 属性在其他插槽中不可用:

<template>
  <MyComponent>
    <!-- 使用显式的默认插槽 -->
    <template #default="{ message }">
      <p>{{ message }}</p>
    </template>

    <template #footer>
      <p>Here's some contact info</p>
    </template>
  </MyComponent>
</template>

provide/inject

Prop 逐级透传问题

通常情况下,当我们需要从父组件向子组件传递数据时,会使用 props。想象一下这样的结构:有一些多层级嵌套的组件,形成了一颗巨大的组件树,而某个深层的子组件需要一个较远的祖先组件中的部分数据。在这种情况下,如果仅使用 props 则必须将其沿着组件链逐级传递下去,这会非常麻烦:

prop-drilling.11201220.png provide 和 inject 可以帮助我们解决这一问题。 [1] 一个父组件相对于其所有的后代组件,会作为依赖提供者。任何后代的组件树,无论层级有多深,都可以注入由父组件提供给整条链路的依赖。

provide-inject.3e0505e4.png

Provide (提供)

import { ref, provide } from 'vue'

const count = ref(0)
provide('key', count)

provide() 函数接收两个参数。第一个参数被称为注入名,可以是一个字符串或是一个 Symbol。后代组件会用注入名来查找期望注入的值。一个组件可以多次调用 provide(),使用不同的注入名,注入不同的依赖值。

第二个参数是提供的值,值可以是任意类型,包括响应式的状态,比如一个 ref。

Inject (注入)

<script setup>
import { inject } from 'vue'

const message = inject('message')
</script>

如果提供的值是一个 ref,注入进来的会是该 ref 对象,而不会自动解包为其内部的值。这使得注入方组件能够通过 ref 对象保持了和供给方的响应性链接。

默认情况下,inject 假设传入的注入名会被某个祖先链上的组件提供。如果该注入名的确没有任何组件提供,则会抛出一个运行时警告。

注入默认值

默认情况下,inject 假设传入的注入名会被某个祖先链上的组件提供。如果该注入名的确没有任何组件提供,则会抛出一个运行时警告。

如果在注入一个值时不要求必须有提供者,那么我们应该声明一个默认值,和 props 类似:

// 如果没有祖先组件提供 "message"
// `value` 会是 "这是默认值"
const value = inject('message', '这是默认值')

在一些场景中,默认值可能需要通过调用一个函数或初始化一个类来取得。为了避免在用不到默认值的情况下进行不必要的计算或产生副作用,我们可以使用工厂函数来创建默认值:

const value = inject('key', () => new ExpensiveClass(), true)

第三个参数表示默认值应该被当作一个工厂函数。

和响应式数据配合使用

当提供 / 注入响应式的数据时,建议尽可能将任何对响应式状态的变更都保持在供给方组件中。这样可以确保所提供状态的声明和变更操作都内聚在同一个组件内,使其更容易维护。

有的时候,我们可能需要在注入方组件中更改数据。在这种情况下,我们推荐在供给方组件内声明并提供一个更改数据的方法函数:

<!-- 在供给方组件内 -->
<script setup>
import { provide, ref } from 'vue'

const location = ref('North Pole')

function updateLocation() {
  location.value = 'South Pole'
}

provide('location', {
  location,
  updateLocation
})
</script>
<!-- 在注入方组件 -->
<script setup>
import { inject } from 'vue'

const { location, updateLocation } = inject('location')
</script>

<template>
  <button @click="updateLocation">{{ location }}</button>
</template>

最后,如果你想确保提供的数据不能被注入方的组件更改,你可以使用 readonly() 来包装提供的值。

<script setup>
import { ref, provide, readonly } from 'vue'

const count = ref(0)
provide('read-only-count', readonly(count))
</script>

父子组件通信、兄弟组件实时通信

父组件向子组件通信

  • 在父组件中为子组件设置props,props包含要相子组件传递的信息。
  • 子组件通过defineprops获取父组件传递的props
<script setup>
import OneSon from "./oneSon.vue";
import { reactive } from "vue";
 
const state = reactive({
  fatherData: "I am from Father.",
});
</script>
 
<template>
  <p>I am Father.</p>
  <OneSon :fatherDataName="state.fatherData"></OneSon>
</template>
 
<style scoped></style>
<script setup>
import { defineProps } from "vue";
 
defineProps({
  fatherDataName: String,
});
</script>
 
<template>
  <p>I am OneSon.</p>
  <p>{{ fatherDataName }}</p>
</template>
 
<style scoped></style>

子组件向父组件通信

  • 子组件中使用emit发送带有参数的事件,(参数包含要向父组件传递的信息)
  • 父组件监听子组件事件,并设置事件处理函数。
//子组件
<script setup>
import { reactive, defineEmits } from "vue";
 
const state = reactive({
  sonData: "I am from Son.",
});
const emit = defineEmits(["sonDataName"]);
const emitSonData = () => {
  emit("sonDataName", state.sonData);
};
</script>
 
<template>
  <p @click="emitSonData">I am OneSon.</p>
</template>
 
<style scoped></style>
//父组件
<script setup>
import OneSon from "./oneSon.vue";
import { reactive } from "vue";
const state = reactive({
  receive: "",
});
const getSonData = (value) => {
  state.receive = value;
};
</script>
 
<template>
  <p>I am Father.</p>
  <OneSon @sonDataName="getSonData"></OneSon>
  <p>{{ state.receive }}</p>
</template>
 
<style scoped></style>

兄弟组件实时通信

  • 子组件1 emit带有参数的事件
  • 子组件2通过defineExpose() 暴露属性。
  • 父组件监听子组件1 emit的事件,获取emit的信息。
  • 父组件中通过设置子组件2 ref属性,获取子组件2,.value.xxxx获取子组件2 属性。
  • 父组件为子组件1 的emit事件设置事件处理函数,
  • 在事件处理函数中将子组件1 emit出来的信息给子组件2。
//子组件1
<script setup>
import { reactive, defineEmits } from "vue";
 
const state = reactive({
  sonData: true,
});
const emit = defineEmits(["sonDataName"]);
const emitSonData = () => {
  emit("sonDataName", state.sonData);
};
</script>
 
<template>
  <p @click="emitSonData">I am OneSon.</p>
</template>
 
<style scoped></style>
//子组件2
<script setup>
import { reactive, defineExpose } from "vue";
 
const state = reactive({
  display: false,
});
const showAnotherSon = (val) => {
  state.display = val;
};
const hidden= () => {
  state.display = false;
};
defineExpose({ showAnotherSon });
</script>
 
<template>
  <p v-if="state.display" @click="hidden()">I am AnotherSon.</p>
</template>
 
<style scoped></style>
//父组件
<script setup>
import OneSon from "./oneSon.vue";
import AnotherSon from "./anotherSon.vue";
import { ref } from "vue";
 
const anotherSon = ref(null);
const getSonData = (val) => {
  anotherSon.value.showAnotherSon(val);
};
</script>
 
<template>
  <p>I am Father.</p>
  <OneSon @sonDataName="getSonData"></OneSon>
  <AnotherSon ref="anotherSon"></AnotherSon>
</template>
 
<style scoped></style>

ref() reactive() toRef() toRefs()

ref 对象是可更改的,也就是说你可以为 .value 赋予新的值。它也是响应式的,即所有对 .value 的操作都将被追踪,并且写操作会触发与之相关的副作用。

如果将一个对象赋值给 ref,那么这个对象将通过 reactive() 转为具有深层次响应式的对象。这也意味着如果对象中包含了嵌套的 ref,它们将被深层地解包。

若要避免这种深层次的转换,请使用 shallowRef() 来替代。
*---------------------------------------------------------------------------------------------
reactive响应式转换是“深层”的:它会影响到所有嵌套的属性。一个响应式对象也将深层地解包任何 ref 属性,同时保持响应性。

值得注意的是,当访问到某个响应式数组或 Map 这样的原生集合类型中的 ref 元素时,不会执行 ref 的解包。

若要避免深层响应式转换,只想保留对这个对象顶层次访问的响应性,请使用 shallowReactive() 作替代。
*---------------------------------------------------------------------------------------------
ref是对原始数据的拷贝,当修改ref数据时,模板中的视图会发生改变,但是原始数据并不会改变。 toRef是对原始数据的引用,修改toRef数据时,原始数据也会发生改变,但是视图并不会更新。

// 按原样返回现有的 ref
toRef(existingRef)

// 创建一个只读的 ref,当访问 .value 时会调用此 getter 函数
toRef(() => props.foo)

// 从非函数的值中创建普通的 ref
// 等同于 ref(1)
toRef(1)

toRefs将一个响应式对象转换为一个普通对象,这个普通对象的每个属性都是指向源对象相应属性的 ref。 在不丢失响应性的情况下对返回的对象进行解构/展开。

参考资源

简介 | Vue.js (vuejs.org)