Vue学习总结

VUE学习笔记

day1

vue框架是什么?

Vue框架是一个MVVM的渐进式JavaScript框架,是初创项目的首选前端框架。它是轻量级的,有很多独立的功能或库,在vue里我们可以根据自己的项目来选用它的一些功能。Vue 的核心库只关注视图层,所以开发者关注的只是m-v的映射关系。

img

其中提到的“渐进式框架”和“自底向上增量开发的设计”是Vue开发的两个概念。

Vue可以在任意其他类型的项目中使用,使用成本较低,更灵活,主张较弱,在Vue的项目中也可以轻松融汇其他的技术来开发,并且因为Vue的生态系统特别庞大,可以找到基本所有类型的工具在vue项目中使用。

Vue 使用一种基于 HTML 的模板语法,使我们能够声明式地将其组件实例的数据绑定到呈现的 DOM 上。所有的 Vue 模板都是语法层面合法的 HTML,可以被符合规范的浏览器和 HTML 解析器解析。

在底层机制中,Vue 会将模板编译成高度优化的 JavaScript 代码。结合响应式系统,当应用状态变更时,Vue 能够智能地推导出需要重新渲染的组件的最少数量,并应用最少的 DOM 操作。

如果你对虚拟 DOM 的概念比较熟悉,并且偏好直接使用 JavaScript,你也可以结合可选的 JSX 支持直接手写渲染函数而不采用模板。但请注意,这将不会享受到和模板同等级别的编译时优化。

创建Vue应用的方法

Vue3
  // 创建应用的方法
  const {createApp} = window.Vue

  // 创建应用
  const app = createApp({
    data(){
      // 定义初始化数据
      // 在vue3中data写法是函数,返回一个对象,对象中写初始化数据
      return {
        msg:"hello 千锋html5大前端"
      }
    }
  })

  // 挂载应用
  app.mount('#app')
Vue2
  new Vue({
   el:'#app',  // 挂载在id为app的dom上
    data:{  // 只有在new Vue时才写为对象,其他时候都写成函数返回对象
       msg:'HELLO 千锋html5大前端'
     }
  })


  new Vue({
    data:{  // 只有在new Vue时才写为对象,其他时候都写成函数返回对象
      msg:'HELLO 千锋html5大前端'
    }
  }).$mount('#app');  // $mount作用和el选项是一样的

Computed

接受一个 getter 函数,返回一个只读的响应式 ref 对象。该 ref 通过 .value 暴露 getter 函数的返回值。它也可以接受一个带有 getset 函数的对象来创建一个可写的 ref 对象。

  • 类型
// 只读
function computed<T>(
  getter: () => T,
  // 查看下方的 "计算属性调试" 链接
  debuggerOptions?: DebuggerOptions
): Readonly<Ref<Readonly<T>>>

// 可写的
function computed<T>(
  options: {
    get: () => T
    set: (value: T) => void
  },
  debuggerOptions?: DebuggerOptions
): Ref<T>

示例

Vue.createApp({

  data(){

   return {

​    count:10

   }

  },

  computed:{ // 计算属性 依赖data里面的变量

   doubleCount(){

​    return this.count*2

   }

  }

 }).mount('#app')
 

组合式Api

const {createApp,ref} = window.Vue;

  createApp({
    setup(){
      const count = ref(10);
      return {  // 必不可少
        count:count
      }
    },
    
  }).mount('#app')

模板语法

文本插值

最基本的数据绑定形式是文本插值,它使用的是“Mustache”语法 (即双大括号):

<span>Message: {{ msg }}</span>

双大括号标签会被替换为相应组件实例中 msg 属性的值。同时每次 msg 属性更改时它也会同步更新。

双大括号会将数据解释为纯文本,而不是 HTML。若想插入 HTML,你需要使用 v-html:

<p>Using text interpolation: {{ rawHtml }}</p>
<p>Using v-html directive: <span v-html="rawHtml"></span></p>
安全警告

在网站上动态渲染任意 HTML 是非常危险的,因为这非常容易造成 XSS 漏洞。请仅在内容安全可信时再使用 v-html,并且永远不要使用用户提供的 HTML 内容。
Attribute 绑定

双大括号不能在 HTML attributes 中使用。想要响应式地绑定一个 attribute,应该使用 v-bind 指令

<div v-bind:id="dynamicId"></div>

v-bind 指令指示 Vue 将元素的 id attribute 与组件的 dynamicId 属性保持一致。如果绑定的值是 null 或者 undefined,那么该 attribute 将会从渲染的元素上移除。

注意隐式类型转换,非空即为真, 确保使用绑定属性。
    <button :disabled="false">按钮</button>
    (显示)



如果属性的值需要拼接字符串完成,且含有变量,可以使用绑定属性结合字符串拼接以及模板字符串
   
     <div v-bind:id="'list'+id">111</div>
     <div v-bind:id="`list-${id}`">222</div>
动态绑定多个值

如果你有像这样的一个包含多个 attribute 的 JavaScript 对象:

const MyObj = {
  id: 'one',
  class: 'two'
}

通过不带参数的 v-bind,你可以将它们绑定到单个元素上:

<div v-bind="MyObj"></div>
简写

因为 v-bind 非常常用,我们提供了特定的简写语法:

<div :id="dynamicId"></div>
tips

绑定在表达式中的方法在组件每次更新时都会被重新调用,因此不应该产生任何副作用,比如改变数据或触发异步操作。

事件绑定

监听事件

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

事件处理器的值可以是:

  1. 内联事件处理器:事件被触发时执行的内联 JavaScript 语句 (与 onclick 类似)。

    内联事件处理器通常用于简单场景,例如:

    js

    const count = ref(0)
    

    template

    <button @click="count++">Add 1</button>
    <p>Count is: {{ count }}</p>
    
  2. 方法事件处理器:一个指向组件上定义的方法的属性名或是路径。

<button v-on:click="add">加1</button>{{count}}<br/>
方法与内联事件判断

模板编译器会通过检查 v-on 的值是否是合法的 JavaScript 标识符或属性访问路径来断定是何种形式的事件处理器。举例来说,foofoo.barfoo['bar'] 会被视为方法事件处理器,而 foo()count++ 会被视为内联事件处理器。

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

有时我们需要在内联事件处理器中访问原生 DOM 事件。你可以向该处理器方法传入一个特殊的 $event 变量,或者使用内联箭头函数:

template

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

<!-- 使用内联箭头函数 -->
<button @click="(event) => warn('Form cannot be submitted yet.', event)">
  Submit
</button>

js

function warn(message, event) {
  // 这里可以访问原生事件
  if (event) {
    event.preventDefault()
  }
  alert(message)
}
事件修饰符

在处理事件时调用 event.preventDefault()event.stopPropagation() 是很常见的。尽管我们可以直接在方法内调用,但如果方法能更专注于数据逻辑而不用去处理 DOM 事件的细节会更好。

为解决这一问题,Vue 为 v-on 提供了事件修饰符。修饰符是用 . 表示的指令后缀,包含以下这些:

  • .stop (阻止冒泡)
  • .prevent (提交事件将不再重新加载页面)
  • .self (阻止元素及其子元素的所有点击事件的默认行为)
  • .capture
  • .once
  • .passive

template

<!-- 单击事件将停止传递 -->
<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>

TIP

使用修饰符时需要注意调用顺序,因为相关代码是以相同的顺序生成的。因此使用 @click.prevent.self
会阻止元素及其子元素的所有点击事件的默认行为@click.self.prevent
则只会阻止对元素本身的点击事件的默认行为。

.capture.once.passive 修饰符与原生 addEventListener
事件
相对应:

template

<!-- 添加事件监听器时,使用 `capture` 捕获模式 -->
<!-- 例如:指向内部元素的事件,在被内部元素处理前,先被外部处理 -->
<div @click.capture="doThis">...</div>

<!-- 点击事件最多被触发一次 -->
<a @click.once="doThis"></a>

<!-- 滚动事件的默认行为 (scrolling) 将立即发生而非等待 `onScroll` 完成 -->
<!-- 以防其中包含 `event.preventDefault()` -->
<div @scroll.passive="onScroll">...</div>

.passive 修饰符一般用于触摸事件的监听器,可以用来改善移动端设备的滚屏性能

条件渲染

v-if #

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

template

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

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

template

<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 区块”。它可以连续多次重复使用:

template

<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-else 类似,一个使用 v-else-if 的元素必须紧跟在一个 v-if 或一个 v-else-if 元素后面。

<template> 上的 v-if #

因为 v-if 是一个指令,他必须依附于某个元素。但如果我们想要切换不止一个元素呢?在这种情况下我们可以在一个 <template> 元素上使用 v-if,这只是一个不可见的包装器元素,最后渲染的结果并不会包含这个 <template> 元素。

template

<template v-if="ok">
  <h1>Title</h1>
  <p>Paragraph 1</p>
  <p>Paragraph 2</p>
</template>

v-elsev-else-if 也可以在 <template> 上使用。

v-show #

另一个可以用来按条件显示一个元素的指令是 v-show。其用法基本一样:

template

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

警告

同时使用 v-ifv-for
不推荐的,因为这样二者的优先级不明显。请查看风格指南获得更多信息。

v-ifv-for 同时存在于一个元素上的时候,v-if
会首先被执行。请查看列表渲染指南获取更多细节。

v-if 优先级更高

列表渲染

v-for="(item,index) of list" :key="index"

list:要循环的数组的变量名

item:循环到的数组元素的变量名

index:循环到的数组元素的索引

列表渲染中的key需要使用列表数据的唯一值

实在不得已的情况下,可以使用数组的索引作为key值

Tip

同时使用 v-ifv-for 是不推荐的,因为这样二者的优先级不明显。

通过 key 管理状态

Vue 默认按照“就地更新”的策略来更新通过 v-for 渲染的元素列表。当数据项的顺序改变时,Vue 不会随之移动 DOM 元素的顺序,而是就地更新每个元素,确保它们在原本指定的索引位置上渲染。

默认模式是高效的,但只适用于列表渲染输出的结果不依赖子组件状态或者临时 DOM 状态 (例如表单输入值) 的情况

为了给 Vue 一个提示,以便它可以跟踪每个节点的标识,从而重用和重新排序现有的元素,你需要为每个元素对应的块提供一个唯一的 key attribute:

template

<div v-for="item in items" :key="item.id">
  <!-- 内容 -->
</div>

当你使用 <template v-for> 时,key 应该被放置在这个 <template> 容器上:

template

<template v-for="todo in todos" :key="todo.name">
  <li>{{ todo.name }}</li>
</template>
注意

key 在这里是一个通过 v-bind 绑定的特殊 attribute。请不要和[在 v-for 中使用对象里所提到的对象属性名相混淆。

vue3里面v-if的优先级高于v-for

vue2里面v-for的优先级高于v-if

表单绑定

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

template

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

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

template

<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 事件:
注意

v-model 会忽略任何表单元素上初始的 valuecheckedselected attribute。它将始终将当前绑定的 JavaScript 状态视为数据的正确来源。你应该在 JavaScript 中使用响应式系统的
API 来声明该初始值。

对于需要使用 IME 的语言 (中文,日文和韩文等),你会发现 v-model 不会在 IME
输入还在拼字阶段时触发更新。如果你的确想在拼字阶段也触发更新,请直接使用自己的 input 事件监听器和 value 绑定而不要使用
v-model

多行文本 #

template

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

注意在 <textarea> 中是不支持插值表达式的。请使用 v-model 来替代:

template

<!-- 错误 -->
<textarea>{{ text }}</textarea>

<!-- 正确 -->
<textarea v-model="text"></textarea>
复选框 #

单一的复选框,绑定布尔类型值:

template

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

也可以将多个复选框绑定到同一个数组或集合的值:

js

const checkedNames = ref([])

template

<div>Checked names: {{ checkedNames }}</div>

<input type="checkbox" id="jack" value="Jack" v-model="checkedNames">
<label for="jack">Jack</label>

<input type="checkbox" id="john" value="John" v-model="checkedNames">
<label for="john">John</label>

<input type="checkbox" id="mike" value="Mike" v-model="checkedNames">
<label for="mike">Mike</label>
单选按钮 #

template

<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>
选择器 #

单个选择器的示例如下:

template

<div>Selected: {{ selected }}</div>

<select v-model="selected">
  <option disabled value="">Please select one</option>
  <option>A</option>
  <option>B</option>
  <option>C</option>
</select>

如果 v-model 表达式的初始值不匹配任何一个选择项,<select> 元素会渲染成一个“未选择”的状态。在 iOS 上,这将导致用户无法选择第一项,因为 iOS 在这种情况下不会触发一个 change
事件。因此,我们建议提供一个空值的禁用选项,如上面的例子所示。

多选 (值绑定到一个数组):

template

<div>Selected: {{ selected }}</div>

<select v-model="selected" multiple>
  <option>A</option>
  <option>B</option>
  <option>C</option>
</select>

选择器的选项可以使用 v-for 动态渲染:

js

const selected = ref('A')

const options = ref([
  { text: 'One', value: 'A' },
  { text: 'Two', value: 'B' },
  { text: 'Three', value: 'C' }
])

template

<select v-model="selected">
  <option v-for="option in options" :value="option.value">
    {{ option.text }}
  </option>
</select>

<div>Selected: {{ selected }}</div>
值绑定 #

对于单选按钮,复选框和选择器选项,v-model 绑定的值通常是静态的字符串 (或者对复选框是布尔值):

template

<!-- `picked` 在被选择时是字符串 "a" -->
<input type="radio" v-model="picked" value="a" />

<!-- `toggle` 只会为 true 或 false -->
<input type="checkbox" v-model="toggle" />

<!-- `selected` 在第一项被选中时为字符串 "abc" -->
<select v-model="selected">
  <option value="abc">ABC</option>
</select>

但有时我们可能希望将该值绑定到当前组件实例上的动态数据。这可以通过使用 v-bind 来实现。此外,使用 v-bind 还使我们可以将选项值绑定为非字符串的数据类型。

复选框 #

template

<input
  type="checkbox"
  v-model="toggle"
  true-value="yes"
  false-value="no" />

true-valuefalse-value 是 Vue 特有的 attributes,仅支持和 v-model 配套使用。这里 toggle 属性的值会在选中时被设为 'yes',取消选择时设为 'no'。你同样可以通过 v-bind 将其绑定为其他动态值:

template

<input
  type="checkbox"
  v-model="toggle"
  :true-value="dynamicTrueValue"
  :false-value="dynamicFalseValue" />

提示

true-valuefalse-value attributes 不会影响 value attribute,因为浏览器在表单提交时,并不会包含未选择的复选框。为了保证这两个值 (例如:“yes”和“no”)
的其中之一被表单提交,请使用单选按钮作为替代。

单选按钮 #

template

<input type="radio" v-model="pick" :value="first" />
<input type="radio" v-model="pick" :value="second" />

pick 会在第一个按钮选中时被设为 first,在第二个按钮选中时被设为 second

选择器选项 #

template

<select v-model="selected">
  <!-- 内联对象字面量 -->
  <option :value="{ number: 123 }">123</option>
</select>

v-model 同样也支持非字符串类型的值绑定!在上面这个例子中,当某个选项被选中,selected 会被设为该对象字面量值 { number: 123 }

修饰符 #
.lazy #

默认情况下,v-model 会在每次 input 事件后更新数据 (IME 拼字阶段的状态例外)。你可以添加 lazy 修饰符来改为在每次 change 事件后更新数据:

template

<!-- 在 "change" 事件后同步更新而不是 "input" -->
<input v-model.lazy="msg" />
.number #

如果你想让用户输入自动转换为数字,你可以在 v-model 后添加 .number 修饰符来管理输入:

template

<input v-model.number="age" />

如果该值无法被 parseFloat() 处理,那么将返回原始值。

number 修饰符会在输入框有 type="number" 时自动启用。

.trim #

如果你想要默认自动去除用户输入内容中两端的空格,你可以在 v-model 后添加 .trim 修饰符:

template

<input v-model.trim="msg" />
组件上的 v-model #

如果你还不熟悉 Vue 的组件,那么现在可以跳过这个部分。

HTML 的内置表单输入类型并不总能满足所有需求。幸运的是,我们可以使用 Vue 构建具有自定义行为的可复用输入组件,并且这些输入组件也支持 v-model

day 2

注册周期钩子 #

举例来说,onMounted 钩子可以用来在组件完成初始渲染并创建 DOM 节点后运行代码:

vue

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

onMounted(() => {
  console.log(`the component is now mounted.`)
})
</script>

还有其他一些钩子,会在实例生命周期的不同阶段被调用,最常用的是 onMountedonUpdatedonUnmounted。所有生命周期钩子的完整参考及其用法请参考 API 索引

当调用 onMounted 时,Vue 会自动将回调函数注册到当前正被初始化的组件实例上。这意味着这些钩子应当在组件初始化时被同步注册。例如,请不要这样做:

js

setTimeout(() => {
  onMounted(() => {
    // 异步注册时当前组件实例已丢失
    // 这将不会正常工作
  })
}, 100)

注意这并不意味着对 onMounted 的调用必须放在 setup()<script setup> 内的词法上下文中。onMounted() 也可以在一个外部函数中调用,只要调用栈是同步的,且最终起源自 setup() 就可以。

生命周期

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Uai0MJAS-1664892924937)(C:\Users\czz\AppData\Roaming\Typora\typora-user-images\image-20221004203332702.png)]

Vue2
const vm = new Vue({
    data:{
      count:10
    },
    methods:{
      add(){
        this.count++;
        if(this.count === 15){
          this.$destroy()
        }
      }
    },
    beforeCreate(){
      // 0个月-马上10个月
      console.log('beforeCreate')
    },
    created(){
      // 有的人在此处请求数据,修改状态 -- 不太建议(请求数据,修改状态 -教育 -- 胎教)
      // 到了10个月
      console.log('created')
    },
    beforeMount(){
      console.log('beforeMount')
    },
    mounted(){
      // 在此处请求数据  -- (请求数据-教育-早教)
      // DOM操作
      // 实例化 new Swiper() 
      // 计时器 延时定时器 (年龄从生下来开始计算)
      console.log('mounted')
    },
    beforeUpdate(){
      console.log('beforeUpdate')
    },
    updated(){
      // DOM操作
      // 实例化
      // 不要请求数据,不要修改状态 会死循环
      console.log('updated')
    },
    beforeDestroy(){
      // 清除定时器,延时器,订阅等
      console.log('beforeDestroy')
    },
    destroyed(){
      console.log('destroyed')
    }

  }).$mount('#app')
Vue3
beforeUnmount(){
      // 清除定时器,延时器,订阅等
      console.log('beforeUnmount')
    },
unmounted(){
      console.log('unmounted')
    }

事件侦听

watch 的第一个参数可以是不同形式的“数据源”:它可以是一个 ref (包括计算属性)、一个响应式对象、一个 getter 函数、或多个数据源组成的数组:

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}`)
})

注意,你不能直接侦听响应式对象的属性值,例如:

js

const obj = reactive({ count: 0 })

// 错误,因为 watch() 得到的参数是一个 number
watch(obj.count, (count) => {
  console.log(`count is: ${count}`)
})

这里需要用一个返回该属性的 getter 函数:

js

// 提供一个 getter 函数
watch(
  () => obj.count,
  (count) => {
    console.log(`count is: ${count}`)
  }
)

深度侦听

深层侦听器 #

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

js

const obj = reactive({ count: 0 })

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

obj.count++

相比之下,一个返回响应式对象的 getter 函数,只有在返回不同的对象时,才会触发回调:

js

watch(
  () => state.someObject,
  () => {
    // 仅当 state.someObject 被替换时触发
  }
)

你也可以给上面这个例子显式地加上 deep 选项,强制转成深层侦听器:

js

watch(
  () => state.someObject,
  (newValue, oldValue) => {
    // 注意:`newValue` 此处和 `oldValue` 是相等的
    // *除非* state.someObject 被整个替换了
  },
  { deep: true }
)

案例

const {createApp} = Vue;
  const app = createApp({
    data(){
      return {
        user:{
          firstName:'张',
          lastName:'三',
          fullName:''
        },
        count:100,
        unwatch:null
      }
    },
    watch:{
      // 侦听失败
      // user:function(newVal,oldVal){
      //   this.user.fullName = newVal.firstName + newVal.lastName;
      // }

      // 深度侦听
      // user:{
      //   deep:true,
      //   handler:function(newVal,oldVal){
      //     console.log(this.$refs.test?.innerHTML)
      //     this.user.fullName = newVal.firstName + newVal.lastName;
      //   },
      //   // 强制立即执行回调函数handler ==> 自动执行一次侦听数据
      //   immediate:true,
      //   // 默认情况下,用户创建的侦听器回调,都会在vue组件更新之前调用
      //   // 这意味着在侦听回调中访问的dom是被vue更新之前的状态
      //   // 在侦听回调中能访问vue更新之后的dom,你需要指明:flush:'post' 选项
      //   flush:'post' // vue3中新增的
      // },


      // 下面的方法也适用于vue2
      "user.firstName":function(newVal,oldVal){
        this.user.fullName = newVal + this.user.lastName;
      },
      "user.lastName":function(newVal,oldVal){
        this.user.fullName = this.user.firstName + newVal
      }
    },
    methods:{
      startWatch(){
        // 开始侦听,赋值给一个函数,用于停止监听
        this.unwatch = this.$watch('count',(newVal,oldVal)=>{
          console.log(newVal,oldVal)
        })
      },
      stopWatch(){
        this.unwatch() // 停止侦听
      }
    }
  })
  app.mount('#app')


  let box = document.querySelector('#box');
  // 如果box有内容,就取他的innerHTML
  // 如果box没有内容,就返回undefined
  // console.log(box?.innerHTML)

模板引用

ref 是一个特殊的 attribute,和 v-for 章节中提到的 key 类似。它允许我们在一个特定的 DOM 元素或子组件实例被挂载后,获得对它的直接引用。这可能很有用,比如说在组件挂载时将焦点设置到一个 input 元素上,或在一个元素上初始化一个第三方库。

组件基础

组件允许我们将 UI 划分为独立的、可重用的部分,并且可以对每个部分进行单独的思考。在实际应用中,组件常常被组织成层层嵌套的树状结构:

组件树

这和我们嵌套 HTML 元素的方式类似,Vue 实现了自己的组件模型,使我们可以在每个组件内封装自定义内容与逻辑。Vue 同样也能很好地配合原生 Web Component。

定义一个组件

当使用构建步骤时,我们一般会将 Vue 组件定义在一个单独的 .vue 文件中,这被叫做单文件组件 (简称 SFC):

vue

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

const count = ref(0)
</script>

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

当不使用构建步骤时,一个 Vue 组件以一个包含 Vue 特定选项的 JavaScript 对象来定义:

js

import { ref } from 'vue'

export default {
  setup() {
    const count = ref(0)
    return { count }
  },
  template: `
    <button @click="count++">
      You clicked me {{ count }} times.
    </button>`
  // 或者 `template: '#my-template-element'`
}

这里的模板是一个内联的 JavaScript 字符串,Vue 将会在运行时编译它。你也可以使用 ID 选择器来指向一个元素 (通常是原生的 <template> 元素),Vue 将会使用其内容作为模板来源。

上例中定义了一个组件,并在一个 .js 文件里默认导出了它自己,也可以通过具名导出在一个文件中导出多个组件。

使用组件

tip

接下来的指引中使用 SFC 语法,无论你是否使用构建步骤,组件相关的概念都是相同的。示例一节中展示了两种场景中的组件使用情况。

要使用一个子组件,我们需要在父组件中导入它。假设我们把计数器组件放在了一个叫做 ButtonCounter.vue 的文件中,这个组件将会以默认导出的形式被暴露给外部。

vue

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

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

通过 <script setup>,导入的组件都在模板中直接可用。

当然,你也可以全局地注册一个组件,使得它在当前应用中的任何组件上都可以使用,而不需要额外再导入。关于组件的全局注册和局部注册两种方式的利弊,我们放在了组件注册这一章节中专门讨论。

组件可以被重用任意多次:

template

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

你会注意到,每当点击这些按钮时,每一个组件都维护着自己的状态,是不同的 count。这是因为每当你使用一个组件,就创建了一个新的实例

在单文件组件中,推荐为子组件使用 PascalCase 的标签名,以此来和原生的 HTML 元素作区分。虽然原生 HTML 标签名是不区分大小写的,但 Vue 单文件组件是可以在编译中区分大小写的。我们也可以使用 /> 来关闭一个标签。

如果你是直接在 DOM 中书写模板 (例如原生 <template> 元素的内容),模板的编译需要遵从浏览器中 HTML 的解析行为。在这种情况下,你应该需要使用 kebab-case 形式并显式地关闭这些组件的标签。

template

<!-- 如果是在 DOM 中书写该模板 -->
<button-counter></button-counter>
<button-counter></button-counter>
<button-counter></button-counter>

全局注册

我们可以使用 Vue 应用实例app.component() 方法,让组件在当前 Vue 应用中全局可用。

js

import { createApp } from 'vue'

const app = createApp({})

app.component(
  // 注册的名字
  'MyComponent',
  // 组件的实现
  {
    /* ... */
  }
)

如果使用单文件组件,你可以注册被导入的 .vue 文件:

js

import MyComponent from './App.vue'

app.component('MyComponent', MyComponent)

app.component() 方法可以被链式调用:

js

app
  .component('ComponentA', ComponentA)
  .component('ComponentB', ComponentB)
  .component('ComponentC', ComponentC)

全局注册的组件可以在此应用的任意组件的模板中使用:

template

<!-- 这在当前应用的任意组件中都可用 -->
<ComponentA/>
<ComponentB/>
<ComponentC/>

所有的子组件也可以使用全局注册的组件,这意味着这三个组件也都可以在彼此内部使用。

vue3.0 局部组件注册

 const Header  = {
    template:'#header',
    data(){
      return {
        msg:'hello vue'
      }
    },
    computed:{
      reverseMsg(){
        return this.msg.split('').reverse().join('')
      }
    }
  }


  const {createApp} = Vue;
  const app = createApp({ 
    components:{  // 03 局部组件注册
      // 组件名: 组件选项
      MyHeader:Header
    }
  })
  app.mount('#app')

vue2.0 局部组件注册

  const Header = {
    template:'#header',
    data(){
      return {
        msg:'hello vue'
      }
    },
    computed:{
      reverseMsg(){
        return this.msg.split('').reverse().join("")
      }
    }
  }

  new Vue({
    components:{
      // 03 局部组件注册
      // 组件名:组件选项
      MyHeader:Header
    }
  }).$mount('#app')

传递 props

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

Props 是一种特别的 attributes,你可以在组件上声明注册。要传递给博客文章组件一个标题,我们必须在组件的 props 列表上声明它。这里要用到 defineProps 宏:

vue

<!-- BlogPost.vue -->
<script setup>
defineProps(['title'])
</script>

<template>
  <h4>{{ title }}</h4>
</template>

defineProps 是一个仅 <script setup> 中可用的编译宏命令,并不需要显式地导入。声明的 props 会自动暴露给模板。defineProps 会返回一个对象,其中包含了可以传递给组件的所有 props:

js

const props = defineProps(['title'])
console.log(props.title)

如果你没有使用 <script setup>,props 必须以 props 选项的方式声明,props 对象会作为 setup() 函数的第一个参数被传入:

js

export default {
  props: ['title'],
  setup(props) {
    console.log(props.title)
  }
}

一个组件可以有任意多的 props,默认情况下,所有 prop 都接受任意类型的值。

当一个 prop 被注册后,可以像这样以自定义 attribute 的形式传递数据给它:

template

<BlogPost title="My journey with Vue" />
<BlogPost title="Blogging with Vue" />
<BlogPost title="Why Vue is so fun" />

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

js

const posts = ref([
  { id: 1, title: 'My journey with Vue' },
  { id: 2, title: 'Blogging with Vue' },
  { id: 3, title: 'Why Vue is so fun' }
])

这种情况下,我们可以使用 v-for 来渲染它们:

template

<BlogPost
  v-for="post in posts"
  :key="post.id"
  :title="post.title"
 />

留意我们是如何使用 v-bind 来传递动态 prop 值的。当事先不知道要渲染的确切内容时,这一点特别有用。

Prop 校验

Vue 组件可以更细致地声明对传入的 props 的校验要求。比如我们上面已经看到过的类型声明,如果传入的值不满足类型要求,Vue 会在浏览器控制台中抛出警告来提醒使用者。这在开发给其他开发者使用的组件时非常有用。

要声明对 props 的校验,你可以向 defineProps() 宏提供一个带有 props 校验选项的对象,例如:

js

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'
    }
  }
})
TIP

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

一些补充细节:

  • 所有 prop 默认都是可选的,除非声明了 required: true
  • Boolean 外的未传递的可选 prop 将会有一个默认值 undefined
  • Boolean 类型的未传递 prop 将被转换为 false。你应该为它设置一个 default 值来确保行为符合预期。
  • 如果声明了 default 值,那么在 prop 的值被解析为 undefined 时,无论 prop 是未被传递还是显式指明的 undefined,都会改为 default 值。

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

如果使用了基于类型的 prop 声明 ,Vue 会尽最大努力在运行时按照 prop 的类型标注进行编译。举例来说,defineProps<{ msg: string }> 会被编译为 { msg: { type: String, required: true }}

运行时类型检查 #

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

  • String
  • Number
  • Boolean
  • Array
  • Object
  • Date
  • Function
  • Symbol

另外,type 也可以是自定义的类或构造函数,Vue 将会通过 instanceof 来检查类型是否匹配。例如下面这个类:

js

class Person {
  constructor(firstName, lastName) {
    this.firstName = firstName
    this.lastName = lastName
  }
}

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

js

defineProps({
  author: Person
})

Vue 会通过 instanceof Person 来校验 author prop 的值是否是 Person 类的一个实例。

组件事件

触发与监听事件

在组件的模板表达式中,可以直接使用 $emit 方法触发自定义事件 (例如:在 v-on 的处理函数中):

template

<!-- MyComponent -->
<button @click="$emit('someEvent')">click me</button>

父组件可以通过 v-on (缩写为 @) 来监听事件:

template

<MyComponent @some-event="callback" />

同样,组件的事件监听器也支持 .once 修饰符:

template

<MyComponent @some-event.once="callback" />

像组件与 prop 一样,事件的名字也提供了自动的格式转换。注意这里我们触发了一个以 camelCase 形式命名的事件,但在父组件中可以使用 kebab-case 形式来监听。与 prop 大小写格式一样,在模板中我们也推荐使用 kebab-case 形式来编写监听器。

TIP

和原生 DOM 事件不一样,组件触发的事件没有冒泡机制。你只能监听直接子组件触发的事件。平级组件或是跨越多层嵌套的组件间通信,应使用一个外部的事件总线,或是使用一个全局状态管理方案

事件参数

有时候我们会需要在触发事件时附带一个特定的值。举例来说,我们想要 <BlogPost> 组件来管理文本会缩放得多大。在这个场景下,我们可以给 $emit 提供一个额外的参数:

template

<button @click="$emit('increaseBy', 1)">
  Increase by 1
</button>

然后我们在父组件中监听事件,我们可以先简单写一个内联的箭头函数作为监听器,此函数会接收到事件附带的参数:

template

<MyButton @increase-by="(n) => count += n" />

或者,也可以用一个组件方法来作为事件处理函数:

template

<MyButton @increase-by="increaseCount" />

该方法也会接收到事件所传递的参数:

js

function increaseCount(n) {
  count.value += n
}
TIP

所有传入 $emit() 的额外参数都会被直接传向监听器。举例来说,$emit('foo', 1, 2, 3) 触发后,监听器函数将会收到这三个参数值。

声明触发的事件

组件要触发的事件可以显式地通过 defineEmits() 宏来声明:

vue

<script setup>
defineEmits(['inFocus', 'submit'])
</script>

我们在 <template> 中使用的 $emit 方法不能在组件的 <script setup> 部分中使用,但 defineEmits() 会返回一个相同作用的函数供我们使用:

vue

<script setup>
const emit = defineEmits(['inFocus', 'submit'])

function buttonClick() {
  emit('submit')
}
</script>

defineEmits()不能在子函数中使用。如上所示,它必须直接放置在 <script setup> 的顶级作用域下。

如果你显式地使用了 setup 函数而不是 <script setup>,则事件需要通过 emits 选项来定义,emit 函数也被暴露在 setup() 的上下文对象上:

js

export default {
  emits: ['inFocus', 'submit'],
  setup(props, ctx) {
    ctx.emit('submit')
  }
}

setup() 上下文对象中的其他属性一样,emit 可以安全地被解构:

js

export default {
  emits: ['inFocus', 'submit'],
  setup(props, { emit }) {
    emit('submit')
  }
}

这个 emits 选项还支持对象语法,它允许我们对触发事件的参数进行验证:

vue

<script setup>
const emit = defineEmits({
  submit(payload) {
    // 通过返回值为 `true` 还是为 `false` 来判断
    // 验证是否通过
  }
})
</script>

如果你正在搭配 TypeScript 使用 <script setup>,也可以使用纯类型标注来声明触发的事件:

vue

<script setup lang="ts">
const emit = defineEmits<{
  (e: 'change', id: number): void
  (e: 'update', value: string): void
}>()
</script>

尽管事件声明是可选的,我们还是推荐你完整地声明所有要触发的事件,以此在代码中作为文档记录组件的用法。同时,事件声明能让 Vue 更好地将事件和透传 attribute 作出区分,从而避免一些由第三方代码触发的自定义 DOM 事件所导致的边界情况。

TIP

如果一个原生事件的名字 (例如 click) 被定义在 emits 选项中,则监听器只会监听组件触发的 click 事件而不会再响应原生的 click 事件。

配合 v-model 使用

自定义事件可以用于开发支持 v-model 的自定义表单组件。回忆一下 v-model 在原生元素上的用法:

template

<input v-model="searchText" />

上面的代码其实等价于下面这段 (编译器会对 v-model 进行展开):

template

<input
  :value="searchText"
  @input="searchText = $event.target.value"
/>

而当使用在一个组件上时,v-model 会被展开为如下的形式:

template

<CustomInput
  :modelValue="searchText"
  @update:modelValue="newValue => searchText = newValue"
/>

要让这个例子实际工作起来,<CustomInput> 组件内部需要做两件事:

  1. 将内部原生 input 元素的 value attribute 绑定到 modelValue prop
  2. 输入新的值时在 input 元素上触发 update:modelValue 事件

这里是相应的代码:

vue

<!-- CustomInput.vue -->
<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>

<template>
  <input
    :value="modelValue"
    @input="$emit('update:modelValue', $event.target.value)"
  />
</template>

现在 v-model 也可以在这个组件上正常工作了:

template

<CustomInput v-model="searchText" />

另一种在组件内实现 v-model 的方式是使用一个可写的,同时具有 getter 和 setter 的计算属性。get 方法需返回 modelValue prop,而 set 方法需触发相应的事件:

vue

<!-- CustomInput.vue -->
<script setup>
import { computed } from 'vue'

const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])

const value = computed({
  get() {
    return props.modelValue
  },
  set(value) {
    emit('update:modelValue', value)
  }
})
</script>

<template>
  <input v-model="value" />
</template>
v-model 的参数 #

默认情况下,v-model 在组件上都是使用 modelValue 作为 prop,并以 update:modelValue 作为对应的事件。我们可以通过给 v-model 指定一个参数来更改这些名字:

template

<MyComponent v-model:title="bookTitle" />

在这个例子中,子组件应声明一个 title prop,并通过触发 update:title 事件更新父组件值:

vue

<!-- MyComponent.vue -->
<script setup>
defineProps(['title'])
defineEmits(['update:title'])
</script>

<template>
  <input
    type="text"
    :value="title"
    @input="$emit('update:title', $event.target.value)"
  />
</template>

在演练场中尝试一下

多个 v-model 绑定 #

利用刚才在 v-model 参数小节中学到的技巧,我们可以在一个组件上创建多个 v-model 双向绑定,每一个 v-model 都会同步不同的 prop:

template

<UserName
  v-model:first-name="first"
  v-model:last-name="last"
/>

vue

<script setup>
defineProps({
  firstName: String,
  lastName: String
})

defineEmits(['update:firstName', 'update:lastName'])
</script>

<template>
  <input
    type="text"
    :value="firstName"
    @input="$emit('update:firstName', $event.target.value)"
  />
  <input
    type="text"
    :value="lastName"
    @input="$emit('update:lastName', $event.target.value)"
  />
</template>
处理 v-model 修饰符 #

在学习输入绑定时,我们知道了 v-model 有一些内置的修饰符,例如 .trim.number.lazy。在某些场景下,你可能想要一个自定义组件的 v-model 支持自定义的修饰符。

我们来创建一个自定义的修饰符 capitalize,它会自动将 v-model 绑定输入的字符串值第一个字母转为大写:

template

<MyComponent v-model.capitalize="myText" />

组件的 v-model 上所添加的修饰符,可以通过 modelModifiers prop 在组件内访问到。在下面的组件中,我们声明了 modelModifiers 这个 prop,它的默认值是一个空对象:

vue

<script setup>
const props = defineProps({
  modelValue: String,
  modelModifiers: { default: () => ({}) }
})

defineEmits(['update:modelValue'])

console.log(props.modelModifiers) // { capitalize: true }
</script>

<template>
  <input
    type="text"
    :value="modelValue"
    @input="$emit('update:modelValue', $event.target.value)"
  />
</template>

注意这里组件的 modelModifiers prop 包含了 capitalize 且其值为 true,因为它在模板中的 v-model 绑定上被使用了。

有了 modelModifiers 这个 prop,我们就可以在原生事件侦听函数中检查它的值,然后决定触发的自定义事件中要向父组件传递什么值。在下面的代码里,我们就是在每次 <input> 元素触发 input 事件时将值的首字母大写:

vue

<script setup>
const props = defineProps({
  modelValue: String,
  modelModifiers: { default: () => ({}) }
})

const emit = defineEmits(['update:modelValue'])

function emitValue(e) {
  let value = e.target.value
  if (props.modelModifiers.capitalize) {
    value = value.charAt(0).toUpperCase() + value.slice(1)
  }
  emit('update:modelValue', value)
}
</script>

<template>
  <input type="text" :value="modelValue" @input="emitValue" />
</template>

对于又有参数又有修饰符的 v-model 绑定,生成的 prop 名将是 arg + "Modifiers"。举例来说:

template

<MyComponent v-model:title.capitalize="myText">

相应的声明应该是:

js

const props = defineProps(['title', 'titleModifiers'])
defineEmits(['update:title'])

console.log(props.titleModifiers) // { capitalize: true }

day3

子传父

<div id="app">
    <my-parent></my-parent>
  </div>

  <template id="child">
    我是子组件
    <!-- 02 通过 this.$emit('自定义事件名',传递给父组件的数据) ,在模板中可以省略this-->
    <button @click="$emit('my-event',2000)">传2000</button>
    <button @click="sendData(5000)">传5000</button>
  </template>

  <template id="parent">
    我是父组件
    <!-- 01 在父组调用子组件的地方,绑定一个自定义的事件,该事件处理函数由父组件实现,默认参数为子组件给父组件传入的值 -->
    <my-child @my-event="getData"></my-child>
  </template>
</body>
<script src="./lib/vue.global.js"></script>
<script>

  const Child = {
    template:'#child',
    methods:{
      sendData(val){
        this.$emit('my-event',val)
      }
    }
  }
  const Parent = {
    template:'#parent',
    components:{
      MyChild:Child
    },
    methods:{
      getData(val){
        console.log('接收到的子组件的数据:'+val)
      }
    }
  }
  const {createApp} = Vue;
  const app = createApp({
    components:{
      MyParent:Parent
    }
  });
  app.mount('#app')
</script>
<body>
  <div id="app">
    <my-parent></my-parent>
  </div>

  <template id="child">
    我是子组件
    <!-- 02 通过 this.$emit('自定义事件名',传递给父组件的数据) ,在模板中可以省略this-->
    <button @click="$emit('my-event',2000)">传2000</button>
    <button @click="sendData(5000)">传5000</button>
  </template>

  <template id="parent">
    我是父组件
    <!-- 01 在父组调用子组件的地方,绑定一个自定义的事件,该事件处理函数由父组件实现,默认参数为子组件给父组件传入的值 -->
    <my-child @my-event="getData"></my-child>
  </template>
</body>
<script src="./lib/vue.global.js"></script>
<script>

  const Child = {
    template:'#child',
    // vue3新增了一个选项,叫做emits里面定义了要触发的父组件的事件名称集合
    // emits数组语法
    // emits:['my-event'],
    // emits对象语法
    emits:{
      'my-event':function(payload){
        // payload:触发自定义事件my-event的时候传递的值
        return payload<3000
      }
    },
    methods:{
      sendData(val){
        this.$emit('my-event',val)
      }
    }
  }
  const Parent = {
    template:'#parent',
    components:{
      MyChild:Child
    },
    methods:{
      getData(val){
        console.log('接收到的子组件的数据:'+val)
      }
    }
  }
  const {createApp} = Vue;
  const app = createApp({
    components:{
      MyParent:Parent
    }
  });
  app.mount('#app')
</script>

透传 Attributes

Attributes 继承 #

“透传 attribute”指的是传递给一个组件,却没有被该组件声明为 propsemits 的 attribute 或者 v-on 事件监听器。最常见的例子就是 classstyleid

当一个组件以单个元素为根作渲染时,透传的 attribute 会自动被添加到根元素上。举例来说,假如我们有一个 <MyButton> 组件,它的模板长这样:

template

<!-- <MyButton> 的模板 -->
<button>click me</button>

一个父组件使用了这个组件,并且传入了 class

template

<MyButton class="large" />

最后渲染出的 DOM 结果是:

html

<button class="large">click me</button>

这里,<MyButton> 并没有将 class 声明为一个它所接受的 prop,所以 class 被视作透传 attribute,自动透传到了 <MyButton> 的根元素上。

classstyle 的合并 #

如果一个子组件的根元素已经有了 classstyle attribute,它会和从父组件上继承的值合并。如果我们将之前的 <MyButton> 组件的模板改成这样:

template

<!-- <MyButton> 的模板 -->
<button class="btn">click me</button>

则最后渲染出的 DOM 结果会变成:

html

<button class="btn large">click me</button>
v-on 监听器继承 #

同样的规则也适用于 v-on 事件监听器:

template

<MyButton @click="onClick" />

click 监听器会被添加到 <MyButton> 的根元素,即那个原生的 <button> 元素之上。当原生的 <button> 被点击,会触发父组件的 onClick 方法。同样的,如果原生 button 元素自身也通过 v-on 绑定了一个事件监听器,则这个监听器和从父组件继承的监听器都会被触发。

深层组件继承 #

有些情况下一个组件会在根节点上渲染另一个组件。例如,我们重构一下 <MyButton>,让它在根节点上渲染 <BaseButton>

template

<!-- <MyButton/> 的模板,只是渲染另一个组件 -->
<BaseButton />

此时 <MyButton> 接收的透传 attribute 会直接继续传给 <BaseButton>

Tip
  1. 透传的 attribute 不会包含 <MyButton> 上声明过的 props 或是针对 emits 声明事件的 v-on 侦听函数,换句话说,声明过的 props 和侦听函数被 <MyButton>“消费”了。
  2. 透传的 attribute 若符合声明,也可以作为 props 传入 <BaseButton>
禁用 Attributes 继承 #

如果你不想要一个组件自动地继承 attribute,你可以在组件选项中设置 inheritAttrs: false

const Base = {

  template:'#base',

  inheritAttrs:false, // 不想要一个组件自动继承attributes,可以在组件选项中设置

 }

ref用到自定义组件,可以获取到子组件的实例

  <my-child ref="childRef"></my-child>
  
  
  console.log(this.$refs.childRef.count);// 10
  console.log(this.$refs.childRef.doubleCount);// 10

$parent

当前组件可能存在的父组件实例,如果当前组件是顶层组件,则为 null

  • 类型

$root

当前组件树的根组件实例。如果当前实例没有父组件,那么这个值就是它自己。

兄弟组件传值 - 中央事件总线

<body>

  <div id="app">
    <my-content></my-content>
    <my-footer></my-footer>
  </div>


  <script src="./lib/vue.js"></script>
  <script>


​    const bus = new Vue();// 中央事件总线 - 电信公司

​    const Content = {
​      template:'<div>{{type}}</div>',
​      data(){
​        return {
​          type:'首页'
​        }
​      },
​      mounted(){
​        // $on用于监听一个自定义事件 - 去电信公司买一个电话,随时接受发给我的短信
​        bus.$on('change-type',(val)=>{
​          this.type = val;
​        })
​      }
​    }

​    const Footer = {
​      template:`<ul>
​        <li @click='changeType("首页")'>首页</li>
​        <li @click='changeType("分类")'>分类</li>
​        <li @click='changeType("购物车")'>购物车</li>
​        <li @click='changeType("我的")'>我的</li>
​      </ul>`,
​      methods:{
​        changeType(val){
​          // $emit用于触发一个指定的事件 - 给指定的电话发短信/打电话
​          bus.$emit('change-type',val)
​        }
​      }
​    }

​    new Vue({
​      components:{
​        MyContent:Content,
​        MyFooter:Footer
​      }
​    }).$mount('#app')
  </script>
</body>

兄弟组件传值 - 状态提升

<body>

  <div id="app">
    <my-content :type="type"></my-content>
    <my-footer @change-type="changeType"></my-footer>
  </div>
  <script src="./lib/vue.global.js"></script>
  <script>
    const Content = {
      template:'<div>{{type}}</div>',
      props:['type']
    }


​    const Footer = {
​      template:`<ul>
​        <li @click='changeType("首页")'>首页</li>
​        <li @click='changeType("分类")'>分类</li>
​        <li @click='changeType("购物车")'>购物车</li>
​        <li @click='changeType("我的")'>我的</li>
​      </ul>`,
​      methods:{
​        changeType(val){
​          this.$emit('change-type',val)
​        }
​      }
​    }

​    const {createApp} = Vue;
​    const app = createApp({
​      components:{
​        MyFooter:Footer,
​        MyContent:Content
​      },
​      data(){
​        return {
​          type:'首页'
​        }
​      },
​      methods:{
​        changeType(val){
​          this.type = val;
​        }
​      }
​    })

​    app.mount('#app')
  </script>
</body>

插槽 Slots

插槽内容与出口 #

在之前的章节中,我们已经了解到组件能够接收任意类型的 JavaScript 值作为 props,但组件要如何接收模板内容呢?在某些场景中,我们可能想要为子组件传递一些模板片段,让子组件在它们的组件中渲染这些片段。

举例来说,这里有一个 <FancyButton> 组件,可以像这样使用:

template

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

<FancyButton> 的模板是这样的:

template

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

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

插槽图示

最终渲染出的 DOM 是这样:

html

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

通过使用插槽,<FancyButton> 仅负责渲染外层的 <button> (以及相应的样式),而其内部的内容由父组件提供。

理解插槽的另一种方式是和下面的 JavaScript 函数作类比,其概念是类似的:

js

// 父元素传入插槽内容
FancyButton('Click me!')

// FancyButton 在自己的模板中渲染插槽内容
function FancyButton(slotContent) {
  return `<button class="fancy-btn">
      ${slotContent}
    </button>`
}

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

template

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

通过使用插槽,<FancyButton> 组件更加灵活和具有可复用性。现在组件可以用在不同的地方渲染各异的内容,但同时还保证都具有相同的样式。

默认内容

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

template

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

如果我们想在父组件没有提供任何插槽内容时在 <button> 内渲染“Submit”,只需要将“Submit”写在 <slot> 标签之间来作为默认内容:

template

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

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

template

<SubmitButton />

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

html

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

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

template

<SubmitButton>Save</SubmitButton>

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

html

<button type="submit">Save</button>
具名插槽 #

有时在一个组件中包含多个插槽出口是很有用的。举例来说,在一个 <BaseLayout> 组件中,有如下模板:

template

<div class="container">
  <header>
    <!-- 标题内容放这里 -->
  </header>
  <main>
    <!-- 主要内容放这里 -->
  </main>
  <footer>
    <!-- 底部内容放这里 -->
  </footer>
</div>

对于这种场景,<slot> 元素可以有一个特殊的 attribute name,用来给各个插槽分配唯一的 ID,以确定每一处要渲染的内容:

template

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

这类带 name 的插槽被称为具名插槽 (named slots)。没有提供 name<slot> 出口会隐式地命名为“default”。

在父组件中使用 <BaseLayout> 时,我们需要一种方式将多个插槽内容传入到各自目标插槽的出口。此时就需要用到具名插槽了:

要为具名插槽传入内容,我们需要使用一个含 v-slot 指令的 <template> 元素,并将目标插槽的名字传给该指令:

template

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

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

具名插槽图示

下面我们给出完整的、向 <BaseLayout> 传递插槽内容的代码,指令均使用的是缩写形式:

template

<BaseLayout>
  <template #header>
    <h1>Here might be a page title</h1>
  </template>

  <template #default>
    <p>A paragraph for the main content.</p>
    <p>And another one.</p>
  </template>

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

当一个组件同时接收默认插槽和具名插槽时,所有位于顶级的非 <template> 节点都被隐式地视为默认插槽的内容。所以上面也可以写成:

template

<BaseLayout>
  <template #header>
    <h1>Here might be a page title</h1>
  </template>

  <!-- 隐式的默认插槽 -->
  <p>A paragraph for the main content.</p>
  <p>And another one.</p>

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

现在 <template> 元素中的所有内容都将被传递到相应的插槽。最终渲染出的 HTML 如下:

html

<div class="container">
  <header>
    <h1>Here might be a page title</h1>
  </header>
  <main>
    <p>A paragraph for the main content.</p>
    <p>And another one.</p>
  </main>
  <footer>
    <p>Here's some contact info</p>
  </footer>
</div>

使用 JavaScript 函数来类比可能更有助于你来理解具名插槽:

js

// 传入不同的内容给不同名字的插槽
BaseLayout({
  header: `...`,
  default: `...`,
  footer: `...`
})

// <BaseLayout> 渲染插槽内容到对应位置
function BaseLayout(slots) {
  return `<div class="container">
      <header>${slots.header}</header>
      <main>${slots.default}</main>
      <footer>${slots.footer}</footer>
    </div>`
}

day4

动态插槽名 #

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

template

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

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

注意这里的表达式和动态指令参数受相同的语法限制

$slots

一个表示父组件所传入插槽的对象。

  • 类型

ts

interface ComponentPublicInstance {
  $slots: { [name: string]: Slot }
}

type Slot = (...args: any[]) => VNode[]

详细信息

通常用于手写渲染函数,但也可用于检测是否存在插槽。

每一个插槽都在 this.$slots 上暴露为一个函数,返回一个 vnode 数组,同时 key 名对应着插槽名。默认插槽暴露为 this.$slots.default

如果插槽是一个作用域插槽,传递给该插槽函数的参数可以作为插槽的 prop 提供给插槽。

组合式 API:依赖注入

provide() #

提供一个值,可以被后代组件注入。

  • 类型

ts

function provide<T>(key: InjectionKey<T> | string, value: T): void

详细信息

provide() 接受两个参数:第一个参数是要注入的 key,可以是一个字符串或者一个 symbol,第二个参数是要注入的值。

当使用 TypeScript 时,key 可以是一个被类型断言为 InjectionKey 的 symbol。InjectionKey 是一个 Vue 提供的工具类型,继承自 Symbol,可以用来同步 provide()inject() 之间值的类型。

与注册生命周期钩子的 API 类似,provide() 必须在组件的 setup() 阶段同步调用。

示例

  • vue

    <script setup>
    import { ref, provide } from 'vue'
    import { fooSymbol } from './injectionSymbols'
    
    // 提供静态值
    provide('foo', 'bar')
    
    // 提供响应式的值
    const count = ref(0)
    provide('count', count)
    
    // 提供时将 Symbol 作为 key
    provide(fooSymbol, count)
    </script>
    

inject() #

注入一个由祖先组件或整个应用 (通过 app.provide()) 提供的值。

  • 类型

ts

// 没有默认值
function inject<T>(key: InjectionKey<T> | string): T | undefined

// 带有默认值
function inject<T>(key: InjectionKey<T> | string, defaultValue: T): T

// 使用工厂函数
function inject<T>(
  key: InjectionKey<T> | string,
  defaultValue: () => T,
  treatDefaultAsFactory: true
): T

详细信息

第一个参数是注入的 key。Vue 会遍历父组件链,通过匹配 key 来确定所提供的值。如果父组件链上多个组件对同一个 key 提供了值,那么离得更近的组件将会“覆盖”链上更远的组件所提供的值。如果没有能通过 key 匹配到值,inject() 将返回 undefined,除非提供了一个默认值。

第二个参数是可选的,即在没有匹配到 key 时使用的默认值。它也可以是一个工厂函数,用来返回某些创建起来比较复杂的值。如果默认值本身就是一个函数,那么你必须将 false 作为第三个参数传入,表明这个函数就是默认值,而不是一个工厂函数。

与注册生命周期钩子的 API 类似,inject() 必须在组件的 setup() 阶段同步调用。

当使用 TypeScript 时,key 可以是一个类型为 InjectionKey 的 symbol。InjectionKey 是一个 Vue 提供的工具类型,继承自 Symbol,可以用来同步 provide()inject() 之间值的类型。

示例

假设有一个父组件已经提供了一些值,如前面 provide() 的例子中所示:

vue

<script setup>
import { inject } from 'vue'
import { fooSymbol } from './injectionSymbols'

// 注入值的默认方式
const foo = inject('foo')

// 注入响应式的值
const count = inject('count')

// 通过 Symbol 类型的 key 注入
const foo2 = inject(fooSymbol)

// 注入一个值,若为空则使用提供的默认值
const bar = inject('foo', 'default value')

// 注入一个值,若为空则使用提供的工厂函数
const baz = inject('foo', () => new Map())

// 注入时为了表明提供的默认值是个函数,需要传入第三个参数
const fn = inject('function', () => {}, false)
</script>

动态组件

<body>
  <div id="app">
    <!-- 动态组件 -->
    <!-- is属性,值为组件标签名 -->
    <component :is="currentType"></component>
    <footer>
      <ul>
        <li @click="currentType='Home'">首页</li>
        <li @click="currentType='Kind'">分类</li>
        <li @click="currentType='Cart'">购物车</li>
        <li @click="currentType='User'">我的</li>
      </ul>
    </footer>
  </div>
</body>
<script src="./lib/vue.global.js"></script>
<script>
  const Home = {
    template:`<div>首页</div>`
  }
  const Kind = {
    template:`<div>
      分类
      <input type='text'/>
    </div>`
  }
  const Cart = {
    template:`<div>购物车</div>`
  }
  const User = {
    template:`<div>我的</div>`
  }
  const {createApp} = Vue;
  const app = createApp({
    components:{
      Home,Kind,Cart,User
    },
    data(){
      return {
        currentType:'Home'
      }
    }
  })
  app.mount('#app')
</script>

KeepAlive

<KeepAlive> 是一个内置组件,它的功能是在多个组件间动态切换时缓存被移除的组件实例。

基本使用 #

在组件基础章节中,我们已经介绍了通过特殊的 <component> 元素来实现动态组件的用法:

template

<component :is="activeComponent" />

默认情况下,一个组件实例在被替换掉后会被销毁。这会导致它丢失其中所有已变化的状态 —— 当这个组件再一次被显示时,会创建一个只带有初始状态的新实例。

在下面的例子中,你会看到两个有状态的组件——A 有一个计数器,而 B 有一个通过 v-model 同步 input 框输入内容的文字展示。尝试先更改一下任意一个组件的状态,然后切走,再切回来:

A

B

Current component: A

count: 0

你会发现在切回来之后,之前已更改的状态都被重置了。

在切换时创建新的组件实例通常是有意义的,但在这个例子中,我们的确想要组件能在被“切走”的时候保留它们的状态。要解决这个问题,我们可以用 <KeepAlive> 内置组件将这些动态组件包装起来:

template

<!-- 非活跃的组件将会被缓存! -->
<KeepAlive>
  <component :is="activeComponent" />
</KeepAlive>

现在,在组件切换时状态也能被保留了:

A

B

Current component: A

count: 0

TIP

DOM 模板中使用时,它应该被写为
<keep-alive>

包含/排除 #

<KeepAlive> 默认会缓存内部的所有组件实例,但我们可以通过 includeexclude prop 来定制该行为。这两个 prop 的值都可以是一个以英文逗号分隔的字符串、一个正则表达式,或是包含这两种类型的一个数组:

template

<!-- 以英文逗号分隔的字符串 -->
<KeepAlive include="a,b">
  <component :is="view" />
</KeepAlive>

<!-- 正则表达式 (需使用 `v-bind`) -->
<KeepAlive :include="/a|b/">
  <component :is="view" />
</KeepAlive>

<!-- 数组 (需使用 `v-bind`) -->
<KeepAlive :include="['a', 'b']">
  <component :is="view" />
</KeepAlive>

它会根据组件的 name 选项进行匹配,所以组件如果想要条件性地被 KeepAlive 缓存,就必须显式声明一个 name 选项。

TIP

在 3.2.34 或以上的版本中,使用 <script setup> 的单文件组件会自动根据文件名生成对应的 name
选项,无需再手动声明。

最大缓存实例数 #

我们可以通过传入 max prop 来限制可被缓存的最大组件实例数。<KeepAlive> 的行为在指定了 max 后类似一个 LRU 缓存:如果缓存的实例数量即将超过指定的那个最大数量,则最久没有被访问的缓存实例将被销毁,以便为新的实例腾出空间。

template

<KeepAlive :max="10">
  <component :is="activeComponent" />
</KeepAlive>
缓存实例的生命周期 #

当一个组件实例从 DOM 上移除但因为被 <KeepAlive> 缓存而仍作为组件树的一部分时,它将变为不活跃状态而不是被卸载。当一个组件实例作为缓存树的一部分插入到 DOM 中时,它将重新被激活

一个持续存在的组件可以通过 onActivated()onDeactivated() 注册相应的两个状态的生命周期钩子:

vue

<script setup>
import { onActivated, onDeactivated } from 'vue'

onActivated(() => {
  // 调用时机为首次挂载
  // 以及每次从缓存中被重新插入时
})

onDeactivated(() => {
  // 在从 DOM 上移除、进入缓存
  // 以及组件卸载时调用
})
</script>
Tip
  • onActivated 在组件挂载时也会调用,并且 onDeactivated 在组件卸载时也会调用。
  • 这两个钩子不仅适用于 <KeepAlive> 缓存的根组件,也适用于缓存树中的后代组件。

案例

<script>
  // 假如首页需要保留组件的状态,跳转到新的页面的时候,然后跳转回来的时候,要确保首页数据的及时性和有效性
  // 以前mounted中请求数据,使用keep-alive时再回到首页的时候mounted不再执行
  // 如果需要获取最新的数据,请在activated中获取
  
  const Home = {
    template:`<div>首页</div>`,
    data(){
      return {
        distance:0
      }
    },
    created(){
      console.log("首页 created")
    },
    mounted(){
      console.log('首页 mounted')
    },
    unmounted(){
      console.log('首页 unmounted')
    },
    activated(){
      console.log('首页 activated')
      console.log('回来的时候,滚动到'+this.distance+"位置")
    },
    deactivated(){
      console.log('首页 deactivated')
      // 假设首页长列表,点击某一项可以进入详情,返回首页,还希望滚动条在原来的位置
      // 以前销毁组件时记录滚动条位置,现在有keep-alive,在deactivated中记录滚动位置
      this.distance = 900;
    }
  }
  const Kind = {
    template:`<div>
      分类
      <input type='text'/>
    </div>`,
    created(){
      console.log("分类 created")
    },
    mounted(){
      console.log('分类 mounted')
    },
    unmounted(){
      console.log('分类 unmounted')
    },
    activated(){
      console.log('分类 activated')
    },
    deactivated(){
      console.log('分类 deactivated')
    }
  }
  const Cart = {
    template:`<div>购物车</div>`,
    created(){
      console.log("购物车 created")
    },
    mounted(){
      console.log('购物车 mounted')
    },
    unmounted(){
      console.log('购物车 unmounted')
    },
    activated(){
      console.log('购物车 activated')
    },
    deactivated(){
      console.log('购物车 deactivated')
    }
  }
  const User = {
    template:`<div>我的</div>`,
    created(){
      console.log("我的 created")
    },
    mounted(){
      console.log('我的 mounted')
    },
    unmounted(){
      console.log('我的 unmounted')
    },
    activated(){
      console.log('我的 activated')
    },
    deactivated(){
      console.log('我的 deactivated')
    }
  }
  const {createApp} = Vue;
  const app = createApp({
    components:{
      Home,Kind,Cart,User
    },
    data(){
      return {
        currentType:'Home'
      }
    }
  })
  app.mount('#app')
</script>

<!-- 动态组件 -->
    <!-- is属性,值为组件标签名 -->
    <!-- 可以通过keep-alive保留组件的状态,避免组件的销毁和重建 -->
    <!-- include可以设置哪些组件需要缓存 -->
    <!-- 用逗号分隔组件名,注意不要加空格 -->
    <!-- <keep-alive include="kindb,userd"> -->

    <!-- 正则表达式 使用 v-bind  -->
    <!-- <keep-alive :include="/^(kindb|userd)$/"> -->

    <!-- 数组,使用v-bind  -->
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值