vue3 3.3.4
https://cn.vuejs.org/guide/introduction.html#what-is-vue
简介
import { createApp } from 'vue'
createApp({
data() {
return {
count: 0
}
}
}).mount('#app')
- 无需构建步骤,渐进式增强静态的 HTML
- 注释:petite-vue,用于渐进式增强静态的 HTML,可以把 html 中部分片段作为 vue 组件
- 在任何页面中作为 Web Components 嵌入
- 注释:Vue 提供了一个 defineCustomElement 方法,用来将 Vue 组件转换成一个继承自 HTMLElement 的自定义元素构造器。
import { defineCustomElement } from "vue";
const MyVueElement = defineCustomElement({
// 这里是同平常一样的 Vue 组件选项
props: {},
emits: {},
template: `...`,
// defineCustomElement 特有的:注入进 shadow root 的 CSS
styles: [`/* inlined css */`],
});
// 注册自定义元素
// 注册之后,所有此页面中的 `<my-vue-element>` 标签
// 都会被升级
window.customElements.define("my-vue-element", MyVueElement);
- Jamstack / 静态站点生成 (SSG)
- 注释:Jamstack / 静态站点生成 (SSG) 是一种构建快速网站的技术,它使用一系列配置、模板和数据,生成静态 HTML 文件及相关资源,并将它们部署到任何支持静态文件的 Web 服务器或 CDN 上
- 选项式 API 是在组合式 API 的基础上实现的
快速上手
- 可以使用导入映射表 (Import Maps) 来告诉浏览器如何定位到导入的 vue
- 如果你更喜欢那些还不支持导入映射表的浏览器,你可以使用 es-module-shims 来进行 polyfill
<script type="importmap">
{
"imports": {
"vue": "https://unpkg.com/vue@3/dist/vue.esm-browser.js"
}
}
</script>
<div id="app">{{ message }}</div>
<script type="module">
import { createApp } from 'vue'
createApp({
data() {
return {
message: 'Hello Vue!'
}
}
}).mount('#app')
</script>
- 如果直接在浏览器中打开了上面的 index.html,你会发现它抛出了一个错误,因为 ES 模块不能通过 file:// 协议工作。为了使其工作,你需要使用本地 HTTP 服务器通过 http:// 协议提供 index.html
- 如果 组件模板是内联的 JavaScript 字符串。如果你正在使用 VSCode,你可以安装 es6-string-html 扩展,然后在字符串前加上一个前缀注释 /html/ 以高亮语法。
创建一个 Vue 应用
- .mount() 方法应该始终在整个应用配置和资源注册完成后被调用。同时请注意,不同于其他资源注册方法,它的返回值是根组件实例而非应用实例。
- 当根组件没有设置 template 选项时,Vue 将自动使用容器的 innerHTML 作为模板。
- DOM 内模板通常用于无构建步骤的 Vue 应用程序。它们也可以与服务器端框架一起使用,其中根模板可能是由服务器动态生成的。
- 应用实例会暴露一个 .config 对象允许我们配置一些应用级的选项,例如定义一个应用级的错误处理器,用来捕获所有子组件上的错误:
app.config.errorHandler = (err) => {
/* 处理错误 */
}
模板语法
- 模板中的表达式将被沙盒化,仅能够访问到有限的全局对象列表。
- 没有显式包含在列表中的全局对象将不能在模板内表达式中访问,例如用户附加在 window 上的属性。然而,你也可以自行在 app.config.globalProperties 上显式地添加它们,供所有的 Vue 表达式使用
- 在指令参数上也可以使用一个 JavaScript 表达式,需要包含在一对方括号内:
- 动态参数中表达式的值应当是一个字符串,或者是 null。特殊值 null 意为显式移除该绑定。其他非字符串的值会触发警告
- 动态参数表达式因为某些字符的缘故有一些语法限制,比如空格和引号,在 HTML attribute 名称中都是不合法的
- 当使用 DOM 内嵌模板 (直接写在 HTML 文件里的模板) 时,我们需要避免在名称中使用大写字母,因为浏览器会强制将其转换为小写
<a v-bind:[attributeName]="url"> ... </a>
<!-- 简写 -->
<a :[attributeName]="url"> ... </a>
响应式基础
- Vue 在组件实例上暴露的内置 API 使用 $ 作为前缀。它同时也为内部属性保留 _ 前缀。因此,你应该避免在顶层 data 上使用任何以这些字符作前缀的属性
- 在 Vue 3 中,数据是基于 JavaScript Proxy(代理) 实现响应式的
- 注释:所以一个对象赋值给响应的属性后,这两个值就不是同一个值了
- 注释:reactive 只是利用 Proxy 拦截了对对象的操作,并返回一个包装过后的对象。在内部对包装后对象的属性操作依然会对原对象进行修改。
- 在某些情况下,我们可能需要动态地创建一个方法函数,比如创建一个预置防抖的事件处理器:
import { debounce } from 'lodash-es'
export default {
methods: {
// 使用 Lodash 的防抖函数
// 注释:这里的 debounce() 会在模块加载后被执行,所以由这个配置项创建的实例,实际上用的都是同一个被 debounce 包装过后的函数
click: debounce(function () {
// ... 对点击的响应 ...
}, 500)
}
}
- 不过这种方法对于被重用的组件来说是有问题的,因为这个预置防抖的函数是 有状态的:它在运行时维护着一个内部状态。如果多个组件实例都共享这同一个预置防抖的函数,那么它们之间将会互相影响。
- 要保持每个组件实例的防抖函数都彼此独立,我们可以改为在 created 生命周期钩子中创建这个预置防抖的函数:
export default {
created() {
// 每个实例都有了自己的预置防抖的处理函数
this.debouncedClick = _.debounce(this.click, 500)
},
unmounted() {
// 最好是在组件卸载时
// 清除掉防抖计时器
this.debouncedClick.cancel()
},
methods: {
click() {
// ... 对点击的响应 ...
}
}
}
- 可以使用 reactive() 函数创建一个响应式对象或数组:
import { reactive } from 'vue'
const state = reactive({ count: 0 })
- 为保证访问代理的一致性,对同一个原始对象调用 reactive() 会总是返回同样的代理对象,而对一个已存在的代理对象调用 reactive() 会返回其本身
- 依靠深层响应性,响应式对象内的嵌套对象依然是代理
- 注释:Proxy 包装的 get 或 set 方法只对浅层有效,vue 递归包装了所有深层,每一层都是一个 Proxy 对象,都具备响应性。
- reactive() API 有两条限制:
- 仅对对象类型有效(对象、数组和 Map、Set 这样的集合类型),而对 string、number 和 boolean 这样的 原始类型 无效
- 因为 Vue 的响应式系统是通过属性访问进行追踪的,因此我们必须始终保持对该响应式对象的相同引用。这意味着我们不可以随意地“替换”一个响应式对象,因为这将导致对初始引用的响应性连接丢失
- 同时这也意味着当我们将响应式对象的属性赋值或解构至本地变量时,或是将该属性传入一个函数时,我们会失去响应性
const state = reactive({ count: 0 })
// n 是一个局部变量,同 state.count
// 失去响应性连接
let n = state.count
// 不影响原始的 state
n++
// count 也和 state.count 失去了响应性连接
let { count } = state
// 不会影响原始的 state
count++
// 该函数接收一个普通数字,并且
// 将无法跟踪 state.count 的变化
// 注释:当在 template 中绑定 callSomeFunction(state.count) 时是具有响应状态的,因为模板中访问了 state 这个 Proxy 对象
// 注释:count 由于是原始类型所有不具备响应性,如果 state.count 是一个对象的话,它是具有响应性的
// 注释:因为 reactive 在包装对象时会递归 Proxy 所有深层对象,所以如果 state.count 是对象的话也是已经被包裹的 Proxy 对象
callSomeFunction(state.count)
- 注释: ref 函数内部会使用 reactive 函数来创建一个响应式的代理对象,这个代理对象会拦截对原始对象和其深层属性的访问和修改,并触发依赖更新
- 仅当 ref 是模板渲染上下文的顶层属性时才适用自动“解包”。 例如, object 是顶层属性,但 object.foo 不是
- 注释:这是在模板中
const object = { foo: ref(1) }
// 下面的表达式将不会像预期的那样工作:
{{ object.foo + 1 }}
- 需要注意的是,如果一个 ref 是文本插值(即一个 {{ }} 符号)计算的最终值,它也将被解包。因此下面的渲染结果将为 1
{{ object.foo }} // 这只是文本插值的一个方便功能,相当于 {{ object.foo.value }}
- 当一个 ref 被嵌套在一个响应式对象中,作为属性被访问或更改时,它会自动解包,因此会表现得和一般的属性一样:
- 注释:这是在 js 中
- 注释:reactive 包装的普通对象({}定义的)在通过 get 获取属性时会对属性指向指的类型进行判断,如果值是一个 ref 对象,会返回 ref.value 的值。同理赋值的时候也是对 ref.value 进行赋值
- 注释:reactive 包装其他对象类型不会有这个特性
- 注释:在模板中绑定 state.count 时等同 reactive 包装的普通对象,不需要写为 state.count.value
- 只有当嵌套在一个深层响应式对象内时,才会发生 ref 解包。当其作为浅层响应式对象的属性被访问时不会解包
- 注释:浅层响应对象是指使用 shallowReactive () 或 shallowReadonly () 创建的响应式对象
const count = ref(0)
const state = reactive({
count
})
console.log(state.count) // 0
state.count = 1
console.log(count.value) // 1
- 跟响应式对象不同,当 ref 作为响应式数组或像 Map 这种原生集合类型的元素被访问时,不会进行解包。
- 注释:这是在 js 中
const books = reactive([ref('Vue 3 Guide')])
// 这里需要 .value
console.log(books[0].value)
const map = reactive(new Map([['count', ref(0)]]))
// 这里需要 .value
console.log(map.get('count').value)
计算属性
- computed() 方法期望接收一个 getter 函数,返回值为一个计算属性 ref。和其他一般的 ref 类似,你可以通过 publishedBooksMessage.value 访问计算结果
类与样式绑定
- 可以在数组中嵌套对象
<div :class="[{ active: isActive }, errorClass]"></div>
- 如果你的组件有多个根元素,你将需要指定哪个根元素来接收这个 class。你可以通过组件的 $attrs 属性来实现指定
<p :class="$attrs.class">Hi!</p>
- 我们还可以给 :style 绑定一个包含多个样式对象的数组。这些对象会被合并后渲染到同一元素上:
<div :style="[baseStyles, overridingStyles]"></div>
- 可以对一个样式属性提供多个 (不同前缀的) 值,举例来说
- 数组仅会渲染浏览器支持的最后一个值。
<div :style="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }"></div>
条件渲染
- 当 v-if 和 v-for 同时存在于一个元素上的时候,v-if 会首先被执行
列表渲染
- Vue 默认按照“就地更新”的策略来更新通过 v-for 渲染的元素列表。当数据项的顺序改变时,Vue 不会随之移动 DOM 元素的顺序,而是就地更新每个元素,确保它们在原本指定的索引位置上渲染。
- 默认模式是高效的,但只适用于列表渲染输出的结果不依赖子组件状态或者临时 DOM 状态 (例如表单输入值) 的情况
- 为了给 Vue 一个提示,以便它可以跟踪每个节点的标识,从而重用和重新排序现有的元素,你需要为每个元素对应的块提供一个唯一的 key attribute
- 推荐在任何可行的时候为 v-for 提供一个 key attribute,除非所迭代的 DOM 内容非常简单 (例如:不包含组件或有状态的 DOM 元素),或者你想有意采用默认行为来提高性能。
- key 绑定的值期望是一个基础类型的值,例如字符串或 number 类型。不要用对象作为 v-for 的 key
- 在计算属性中使用 reverse() 和 sort() 的时候务必小心!这两个方法将变更原始数组,计算函数中不应该这么做。
事件处理
- 有时我们需要在内联事件处理器中访问原生 DOM 事件。你可以向该处理器方法传入一个特殊的 $event 变量,或者使用内联箭头函数:
- 注释:内联事件会被包装成如下形式
warn('Form cannot be submitted yet.', $event)
=>(event)=>this.warn('Form cannot be submitted yet.', event)
- 注释:函数名事件感觉也会被包装
- 注释:内联事件会被包装成如下形式
<!-- 使用特殊的 $event 变量 -->
<button @click="warn('Form cannot be submitted yet.', $event)">
Submit
</button>
<!-- 使用内联箭头函数 -->
<button @click="(event) => warn('Form cannot be submitted yet.', event)">
Submit
</button>
- 事件修饰符
- .capture 添加事件监听器时,使用
capture
捕获模式 - .passive 告诉浏览器你不想阻止事件的默认行为,从而提高性能和用户体验
- 注释:onScroll 滚动事件会在每一帧滚动动画结束后被触发,且在一个滚动过程中会被多次触发,当 onScroll 有事件时会阻碍下一帧动画导致不顺畅
- .capture 添加事件监听器时,使用
- 可以直接使用 KeyboardEvent.key 暴露的按键名称作为修饰符,但需要转为 kebab-case 形式。
<input @keyup.page-down="onPageDown" />
- 系统按键修饰符和常规按键不同。与 keyup 事件一起使用时,该按键必须在事件发出时处于按下状态。换句话说,keyup.ctrl 只会在你仍然按住 ctrl 但松开了另一个键时被触发。若你单独松开 ctrl 键将不会触发。
- .exact 修饰符允许控制触发一个事件所需的确定组合的系统按键修饰符。
<!-- 当按下 Ctrl 时,即使同时按下 Alt 或 Shift 也会触发 -->
<button @click.ctrl="onClick">A</button>
<!-- 仅当按下 Ctrl 且未按任何其他键时才会触发 -->
<button @click.ctrl.exact="onCtrlClick">A</button>
<!-- 仅当没有按下任何系统按键时触发 -->
<button @click.exact="onClick">A</button>
表单输入绑定
- v-model 还可以用于各种不同类型的输入,<textarea>、<select> 元素。它会根据所使用的元素自动使用对应的 DOM 属性和事件组合
- v-model 会忽略任何表单元素上初始的 value、checked 或 selected attribute。它将始终将当前绑定的 JavaScript 状态视为数据的正确来源
- 注释:v-model是语法糖,在编译时根据标签类型编译成不同的语法
- 对于需要使用 IME 的语言 (中文,日文和韩文等),你会发现 v-model 不会在 IME 输入还在拼字阶段时触发更新。如果你的确想在拼字阶段也触发更新,请直接使用自己的 input 事件监听器和 value 绑定而不要使用 v-model
- 单一的复选框,绑定布尔类型值
// checked是布尔值
<input type="checkbox" id="checkbox" v-model="checked" />
- 可以将多个复选框绑定到同一个数组或集合的值
- 注释:复选框有两种使用方式,绑定布尔值(是否选中)或绑定一个数组(value是否包含在数组中)
// const checkedNames = ref([])
<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>
- 如果 v-model 表达式的初始值不匹配任何一个选择项,<select> 元素会渲染成一个“未选择”的状态。在 iOS 上,这将导致用户无法选择第一项,因为 iOS 在这种情况下不会触发一个 change 事件。因此,我们建议提供一个空值的禁用选项,如上面的例子所示。
- 注释:当 option 没有 value 属性时,选中值为 option 的 htmlText
<select v-model="selected">
<option disabled value="">Please select one</option>
<option>A</option>
<option>B</option>
<option>C</option>
</select>
- true-value 和 false-value 是 Vue 特有的 attributes,仅支持和 v-model 配套使用。这里 toggle 属性的值会在选中时被设为 'yes',取消选择时设为 'no'。
- 注释:复选框在没有 value 属性时,绑定值默认为布尔值
<input
type="checkbox"
v-model="toggle"
true-value="yes"
false-value="no" />
- true-value 和 false-value attributes 不会影响 value attribute,因为浏览器在表单提交时,并不会包含未选择的复选框。为了保证这两个值 (例如:“yes”和“no”) 的其中之一被表单提交,请使用单选按钮作为替代。
- 注释:浏览器表单提交时会提交复选框的 value 属性,未选中时不会提交这个复选框项,当选中时复选框的 value 会被设置为 true-value 或 false-value 绑定值,符合浏览器表单提交的默认行为
- pick 会在第一个按钮选中时被设为 first,在第二个按钮选中时被设为 second
<input type="radio" v-model="pick" :value="first" />
<input type="radio" v-model="pick" :value="second" />
- v-model 同样也支持非字符串类型的值绑定!在上面这个例子中,当某个选项被选中,selected 会被设为该对象字面量值
- 注释:value 的绑定值和 selected 的选中值是同一个内存对象
<select v-model="selected">
<!-- 内联对象字面量 -->
<option :value="{ number: 123 }">123</option>
</select>
- 默认情况下,v-model 会在每次 input 事件后更新数据 (IME 拼字阶段的状态例外)。你可以添加 lazy 修饰符来改为在每次 change 事件后更新数据
<!-- 在 "change" 事件后同步更新而不是 "input" -->
<input v-model.lazy="msg" />
- 如果你想让用户输入自动转换为数字,你可以在 v-model 后添加 .number 修饰符来管理输入:
- 如果该值无法被 parseFloat() 处理,那么将返回原始值。
- number 修饰符会在输入框有 type="number" 时自动启用。
<input v-model.number="age" />
- 如果你想要默认自动去除用户输入内容中两端的空格,你可以在 v-model 后添加 .trim 修饰符:
<input v-model.trim="msg" />
生命周期
- 注释:
- new Vue 创建应用实例
- 触发 beforeCreate 生命周期
- 应用实例.mount 创建根组件实例,调用 _init 进行初始化
- 合并选项
- 初始化生命周期,设置父子组件关系
- 初始化事件
- 初始化渲染,创建一个空得 VNode,赋值给 vm._vnode
- 初始化状态
- 创建 Watcher 实例,用于订阅数据变化
- 触发 created 生命周期
- 调用 render 函数生成虚拟 dom
- 发现虚拟 dom 中有组件,塞入实例化队列
- 实例树包含所有组件实例
- 虚拟dom树包括用于生成浏览器标准dom的虚拟节点
- render函数应该是先创建了对应的虚拟dom,然后把这个虚拟dom传入构造函数创建实例,再把实例塞入实例队列等待初始化,当实例初始化结束调用 render 时,用 render 返回结果代替传入的虚拟 dom
- 发现虚拟 dom 中有组件,塞入实例化队列
- 检查实例化队列,为空时,调用 patch 函数更新到真实 dom
setTimeout(() => {
onMounted(() => {
// 异步注册时当前组件实例已丢失
// 这将不会正常工作
})
}, 100)
侦听器
- watch 的第一个参数可以是不同形式的“数据源”:它可以是一个 ref (包括计算属性)、一个响应式对象、一个 getter 函数、或多个数据源组成的数组
// 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}`)
})
- 不能直接侦听响应式对象的属性值,例如:
- 注释:这个属性的值不能是个基础类型,因为基础类型不具有响应性。如果值是个对象的,在 reactive 包装时这个值也被递归包装了,具有响应性
const obj = reactive({ count: 0 })
// 错误,因为 watch() 得到的参数是一个 number
watch(obj.count, (count) => {
console.log(`count is: ${count}`)
})
// 提供一个 getter 函数
watch(
() => obj.count,
(count) => {
console.log(`count is: ${count}`)
}
)
- 直接给 watch() 传入一个响应式对象,会隐式地创建一个深层侦听器——该回调函数在所有嵌套的变更时都会被触发:
const obj = reactive({ count: 0 })
watch(obj, (newValue, oldValue) => {
// 在嵌套的属性变更时触发
// 注意:`newValue` 此处和 `oldValue` 是相等的
// 因为它们是同一个对象!
})
obj.count++
- 相比之下,一个返回响应式对象的 getter 函数,只有在返回不同的对象时,才会触发回调:
- 注释:下例中计算属性的返回===state.someObject,即具有响应性
- 注释:在 watch 计算属性时应该是做了特殊处理,不会监听内部的改变
watch(
() => state.someObject,
() => {
// 仅当 state.someObject 被替换时触发
}
)
- 也可以给上面这个例子显式地加上 deep 选项,强制转成深层侦听器:
watch(
() => state.someObject,
(newValue, oldValue) => {
// 注意:`newValue` 此处和 `oldValue` 是相等的
// *除非* state.someObject 被整个替换了
},
{ deep: true }
)
- watchEffect() 允许我们自动跟踪回调的响应式依赖。上面的侦听器可以重写为:
- 这个例子中,回调会立即执行,不需要指定 immediate: true。在执行期间,它会自动追踪 todoId.value 作为依赖(和计算属性类似)。每当 todoId.value 变化时,回调会再次执行。有了 watchEffect(),我们不再需要明确传递 todoId 作为源值。
- watchEffect 仅会在其同步执行期间,才追踪依赖。在使用异步回调时,只有在第一个 await 正常工作前访问到的属性才会被追踪
watchEffect(async () => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/todos/${todoId.value}`
)
data.value = await response.json()
})
- watch 只追踪明确侦听的数据源。它不会追踪任何在回调中访问到的东西。另外,仅在数据源确实改变时才会触发回调。watch 会避免在发生副作用时追踪依赖,因此,我们能更加精确地控制回调函数的触发时机。
- 注释:watch 监听的对象被 set 相同的值时不会触发,但是 watchEffect 会
- 默认情况下,用户创建的侦听器回调,都会在 Vue 组件更新之前被调用。这意味着你在侦听器回调中访问的 DOM 将是被 Vue 更新之前的状态。
- 如果想在侦听器回调中能访问被 Vue 更新之后的 DOM,你需要指明 flush: 'post' 选项:
- 后置刷新的 watchEffect() 有个更方便的别名 watchPostEffect()
- 注释:optionAPI也支持
watch(source, callback, {
flush: 'post'
})
watchEffect(callback, {
flush: 'post'
})
watchPostEffect(() => {
/* 在 Vue 更新后执行 */
})
- 一个关键点是,侦听器必须用同步语句创建:如果用异步回调创建一个侦听器,那么它不会绑定到当前组件上,你必须手动停止它,以防内存泄漏。
- 要手动停止一个侦听器,请调用 watch 或 watchEffect 返回的函数:
const unwatch = watchEffect(() => {})
// ...当该侦听器不再需要时
unwatch()
optionAPI
- watch 默认是浅层的:被侦听的属性,仅在被赋新值时,才会触发回调函数——而嵌套属性的变化不会触发。如果想侦听所有嵌套的变更,你需要深层侦听器:
- 回调函数的初次执行就发生在 created 钩子之前。Vue 此时已经处理了 data、computed 和 methods 选项,所以这些属性在第一次调用时就是可用的。
模板引用
- 应该注意的是,ref 数组并不保证与源数组相同的顺序。
- 注释:因为 ref 数组是根据组件的挂载顺序来更新的,而不是根据源数组的顺序
- 除了使用字符串值作名字,ref attribute 还可以绑定为一个函数,会在每次组件更新时都被调用。该函数会收到元素引用作为其第一个参数:
- 注意我们这里需要使用动态的 :ref 绑定才能够传入一个函数
- 当绑定的元素被卸载时,函数也会被调用一次,此时的 el 参数会是 null
- 注释:optionAPI也支持
<input :ref="(el) => { /* 将 el 赋值给一个数据属性或 ref 变量 */ }">
- 使用了 <script setup> 的组件是默认私有的:一个父组件无法访问到一个使用了 <script setup> 的子组件中的任何东西,除非子组件在其中通过 defineExpose 宏显式暴露:
<script setup>
import { ref } from 'vue'
const a = 1
const b = ref(2)
// 像 defineExpose 这样的编译器宏不需要导入
defineExpose({
a,
b
})
// 当父组件通过模板引用获取到了该组件的实例时,得到的实例类型为 { a: number, b: number } (ref 都会自动解包,和一般的实例一样)。
</script>
optionAPI
- expose 选项可以用于限制对子组件实例的访问:
export default {
expose: ['publicData', 'publicMethod'],
data() {
return {
publicData: 'foo',
privateData: 'bar'
}
},
methods: {
publicMethod() {
/* ... */
},
privateMethod() {
/* ... */
}
}
}
// 在上面这个例子中,父组件通过模板引用访问到子组件实例后,仅能访问 publicData 和 publicMethod。
组件基础
- 在单文件组件中,推荐为子组件使用 PascalCase 的标签名,以此来和原生的 HTML 元素作区分。虽然原生 HTML 标签名是不区分大小写的,但 Vue 单文件组件是可以在编译中区分大小写的
- defineProps 会返回一个对象,其中包含了可以传递给组件的所有 props:
const props = defineProps(['title'])
console.log(props.title)
- defineEmits 仅可用于 <script setup> 之中,并且不需要导入,它返回一个等同于 $emit 方法的 emit 函数。
<script setup>
const emit = defineEmits(['enlarge-text'])
emit('enlarge-text')
</script>
- 如果你没有在使用 <script setup>,你可以通过 emits 选项定义组件会抛出的事件
- 可以从 setup() 函数的第二个参数,即 setup 上下文对象上访问到 emit 函数
- 例子中,被传给 :is 的值可以是以下几种:
- 被注册的组件名
- 导入的组件对象
<!-- currentTab 改变时组件也改变 -->
<component :is="tabs[currentTab]"></component>
- 在 DOM 模板中,我们必须显式地写出关闭标签
- 这是由于 HTML 只允许一小部分特殊的元素省略其关闭标签
<my-component></my-component>
- 某些 HTML 元素对于放在其中的元素类型有限制
- 这将导致在使用带有此类限制元素的组件时出现问题。例如:
<table>
<blog-post-row></blog-post-row>
</table>
- 可以使用特殊的 is attribute 作为一种解决方案:
- 当使用在原生 HTML 元素上时,is 的值必须加上前缀 vue: 才可以被解析为一个 Vue 组件。这一点是必要的,为了避免和原生的自定义内置元素相混淆。
<table>
<tr is="vue:blog-post-row"></tr>
</table>
注册
- 可以使用 Vue 应用实例的 app.component() 方法,让组件在当前 Vue 应用中全局可用
- 使用 PascalCase 作为组件名的注册格式
- PascalCase 是合法的 JavaScript 标识符。这使得在 JavaScript 中导入和注册组件都很容易,同时 IDE 也能提供较好的自动补全。
- <PascalCase /> 在模板中更明显地表明了这是一个 Vue 组件,而不是原生 HTML 元素。同时也能够将 Vue 组件和自定义元素 (web components) 区分开来。
- 为了方便,Vue 支持将模板中使用 kebab-case 的标签解析为使用 PascalCase 注册的组件
Props
- 注意传递给 defineProps() 的参数和提供给 props 选项的值是相同的,两种声明方式背后其实使用的都是 prop 选项。
- 如果一个 prop 的名字很长,应使用 camelCase 形式,因为它们是合法的 JavaScript 标识符,可以直接在模板的表达式中使用,也可以避免在作为属性 key 名时必须加上引号
- 虽然理论上你也可以在向子组件传递 props 时使用 camelCase 形式 (使用 DOM 模板时例外),但实际上为了和 HTML attribute 对齐,我们通常会将其写为 kebab-case 形式:
- prop 被用于传入初始值;而子组件想在之后将其作为一个局部数据属性。在这种情况下,最好是新定义一个局部数据属性,从 props 上获取初始值即可:
- 注释:optionAPI也适用
const props = defineProps(['initialCounter'])
// 计数器只是将 props.initialCounter 作为初始值
// 像下面这样做就使 prop 和后续更新无关了
const counter = ref(props.initialCounter)
- 当对象或数组作为 props 被传入时,虽然子组件无法更改 props 绑定,但仍然可以更改对象或数组内部的值。这是因为 JavaScript 的对象和数组是按引用传递,而对 Vue 来说,禁止这样的改动,虽然可能生效,但有很大的性能损耗,比较得不偿失。
- 这种更改的主要缺陷是它允许了子组件以某种不明显的方式影响父组件的状态,可能会使数据流在将来变得更难以理解。在最佳实践中,你应该尽可能避免这样的更改,除非父子组件在设计上本来就需要紧密耦合
- 注释:性能损耗似乎差不多
- type 也可以是自定义的类或构造函数,Vue 将会通过 instanceof 来检查类型是否匹配。例如下面这个类:
defineProps({
author: Person
})
- 为了更贴近原生 boolean attributes 的行为,声明为 Boolean 类型的 props 有特别的类型转换规则。
- 当一个 prop 被声明为允许多种类型时,无论声明类型的顺序如何,Boolean 类型的特殊转换规则都会被应用。
<!-- 等同于传入 :disabled="true" -->
<MyComponent disabled />
<!-- 等同于传入 :disabled="false" -->
<MyComponent />
事件
- emits 选项还支持对象语法,它允许我们对触发事件的参数进行验证
<script setup>
const emit = defineEmits({
submit(payload) {
// 通过返回值为 `true` 还是为 `false` 来判断
// 验证是否通过
}
})
</script>
- 事件声明能让 Vue 更好地将事件和透传 attribute 作出区分,从而避免一些由第三方代码触发的自定义 DOM 事件所导致的边界情况
- 如果一个原生事件的名字 (例如 click) 被定义在 emits 选项中,则监听器只会监听组件触发的 click 事件而不会再响应原生的 click 事件
组件 v-model
- 当使用在一个组件上时,v-model 会被展开为如下的形式:
<CustomInput
:modelValue="searchText"
@update:modelValue="newValue => searchText = newValue"
/>
- 可以通过给 v-model 指定一个参数来更改这些名字
<MyComponent v-model:title="bookTitle" />
- 组件的 v-model 上所添加的修饰符,可以通过 modelModifiers prop 在组件内访问到
- 在下面的组件中,我们声明了 modelModifiers 这个 prop,它的默认值是一个空对象
- 对于又有参数又有修饰符的 v-model 绑定,生成的 prop 名将是 arg + "Modifiers"
<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>
透传 Attributes
- 如果你不想要一个组件自动地继承 attribute,你可以在组件选项中设置 inheritAttrs: false
- 如果你使用了 <script setup>,你需要一个额外的 <script> 块来书写这个选项声明
<script>
// 使用普通的 <script> 来声明选项
export default {
inheritAttrs: false
}
</script>
<script setup>
// ...setup 部分逻辑
</script>
- 从 3.3 开始你也可以直接在 <script setup> 中使用 defineOptions
<script setup>
defineOptions({
inheritAttrs: false
})
// ...setup 逻辑
</script>
- $attrs 对象包含了除组件所声明的 props 和 emits 之外的所有其他 attribute,例如 class,style,v-on 监听器等等。
- 和 props 有所不同,透传 attributes 在 JavaScript 中保留了它们原始的大小写,所以像 foo-bar 这样的一个 attribute 需要通过 $attrs['foo-bar'] 来访问
- 像 @click 这样的一个 v-on 事件监听器将在此对象下被暴露为一个函数 $attrs.onClick
- 注释:v-bind 也可以用于绑定事件只是要以 onClick 的方式
<div class="btn-wrapper">
<button class="btn" v-bind="$attrs">click me</button>
</div>
- 可以在 <script setup> 中使用 useAttrs() API 来访问一个组件的所有透传 attribute:
<script setup>
import { useAttrs } from 'vue'
const attrs = useAttrs()
</script>
- 如果没有使用 <script setup>,attrs 会作为 setup() 上下文对象的一个属性暴露
export default {
setup(props, ctx) {
// 透传 attribute 被暴露为 ctx.attrs
console.log(ctx.attrs)
}
}
- 虽然这里的 attrs 对象总是反映为最新的透传 attribute,但它并不是响应式的 (考虑到性能因素)。你不能通过侦听器去监听它的变化。
- 如果你需要响应性,可以使用 prop。或者你也可以使用 onUpdated() 使得在每次更新时结合最新的 attrs 执行副作用
插槽
- Vue 组件的插槽机制是受原生 Web Component <slot> 元素的启发而诞生
- 要为具名插槽传入内容,我们需要使用一个含 v-slot 指令的 <template> 元素,并将目标插槽的名字传给该指令:
<BaseLayout>
<template v-slot:header>
<!-- header 插槽的内容放这里 -->
</template>
</BaseLayout>
- v-slot 有对应的简写 #,因此 <template v-slot:header> 可以简写为 <template #header>
- 动态指令参数在 v-slot 上也是有效的,即可以定义下面这样的动态插槽名
<base-layout>
<template v-slot:[dynamicSlotName]>
...
</template>
<!-- 缩写为 -->
<template #[dynamicSlotName]>
...
</template>
</base-layout>
- 默认插槽如何接受 props,通过子组件标签上的 v-slot 指令,直接接收到了一个插槽 props 对象:
<MyComponent v-slot="slotProps">
{{ slotProps.text }} {{ slotProps.count }}
</MyComponent>
- 如果你同时使用了具名插槽与默认插槽,则需要为默认插槽使用显式的 <template> 标签。尝试直接为组件添加 v-slot 指令将导致编译错误。这是为了避免因默认插槽的 props 的作用域而困惑
- 一些组件可能只包括了逻辑而不需要自己渲染内容,视图输出通过作用域插槽全权交给了消费者组件。我们将这种类型的组件称为无渲染组件。
- 但大部分能用无渲染组件实现的功能都可以通过组合式 API 以另一种更高效的方式实现,并且还不会带来额外组件嵌套的开销
依赖注入
- 如果不使用 <script setup>,请确保 provide() 是在 setup() 同步调用的:
import { provide } from 'vue'
export default {
setup() {
provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
}
}
- 除了在一个组件中提供依赖,我们还可以在整个应用层面提供依赖:
import { createApp } from 'vue'
const app = createApp({})
app.provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
- 在一些场景中,默认值可能需要通过调用一个函数或初始化一个类来取得。为了避免在用不到默认值的情况下进行不必要的计算或产生副作用,我们可以使用工厂函数来创建默认值:
- 注释:默认值 () => new ExpensiveClass() 在当能够查找到对应 key 的 provide 时,是不会运行的
- 注释:第三个参数用以声明 inject 的值是否函数
const value = inject('key', () => new ExpensiveClass())
- 当提供 / 注入响应式的数据时,建议尽可能将任何对响应式状态的变更都保持在供给方组件中。这样可以确保所提供状态的声明和变更操作都内聚在同一个组件内,使其更容易维护。
- 有的时候,我们可能需要在注入方组件中更改数据。在这种情况下,我们推荐在供给方组件内声明并提供一个更改数据的方法函数
- 最后,如果你想确保提供的数据不能被注入方的组件更改,你可以使用 readonly() 来包装提供的值。
<script setup>
import { ref, provide, readonly } from 'vue'
const count = ref(0)
provide('read-only-count', readonly(count))
</script>
异步组件
- 提供了 defineAsyncComponent 方法来实现此功能
- 最后得到的 AsyncComp 是一个外层包装过的组件,仅在页面需要它渲染时才会调用加载内部实际组件的函数。
- 它会将接收到的 props 和插槽传给内部组件,所以你可以使用这个异步的包装组件无缝地替换原始组件,同时实现延迟加载。
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent(() => {
return new Promise((resolve, reject) => {
// ...从服务器获取组件
resolve(/* 获取到的组件 */)
})
})
// ... 像使用其他一般组件一样使用 `AsyncComp`
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent(() =>
import('./components/MyComponent.vue')
)
- 异步操作不可避免地会涉及到加载和错误状态,因此 defineAsyncComponent() 也支持在高级选项中处理这些状态
- 在加载组件显示之前有一个默认的 200ms 延迟——这是因为在网络状况较好时,加载完成得很快,加载组件和最终组件之间的替换太快可能产生闪烁,反而影响用户感受。
const AsyncComp = defineAsyncComponent({
// 加载函数
loader: () => import('./Foo.vue'),
// 加载异步组件时使用的组件
loadingComponent: LoadingComponent,
// 展示加载组件前的延迟时间,默认为 200ms
delay: 200,
// 加载失败后展示的组件
errorComponent: ErrorComponent,
// 如果提供了一个 timeout 时间限制,并超时了
// 也会显示这里配置的报错组件,默认值是:Infinity
timeout: 3000
})
- 异步组件可以搭配内置的 <Suspense> 组件一起使用
组合式函数
- toValue() is an API added in 3.3. It is designed to normalize refs or getters into values. If the argument is a ref, it returns the ref's value; if the argument is a function, it will call the function and return its return value. Otherwise, it returns the argument as-is. It works similarly to unref(), but with special treatment for functions.
- toValue()是一个在3.3版本中添加的API。它的目的是将refs或者函数标准化为值。如果参数是一个ref,它就返回ref的值;如果参数是一个函数,它就调用函数并返回其返回值。否则,它就原样返回参数。它的工作方式类似于unref(),但是对函数有特殊的处理。
// fetch.js
import { ref, watchEffect, toValue } from 'vue'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
watchEffect(() => {
// reset state before fetching..
data.value = null
error.value = null
// toValue() unwraps potential refs or getters
fetch(toValue(url))
.then((res) => res.json())
.then((json) => (data.value = json))
.catch((err) => (error.value = err))
})
return { data, error }
}
- 组合式函数约定用驼峰命名法命名,并以“use”作为开头
- 我们推荐的约定是组合式函数始终返回一个包含多个 ref 的普通的非响应式对象,这样该对象在组件中被解构为 ref 之后仍可以保持响应性:
- 在组合式函数中的确可以执行副作用 (例如:添加 DOM 事件监听器或者请求数据),但请注意以下规则:
- 确保在 onUnmounted() 时清理副作用。举例来说,如果一个组合式函数设置了一个事件监听器,它就应该在 onUnmounted() 中被移除
- Access to an active component instance is necessary so that:组件实例化时
- 将计算属性和监听器注册到该组件实例上,以便在该组件被卸载时停止监听,避免内存泄漏
- <script setup> 是唯一在调用 await 之后仍可调用组合式函数的地方。编译器会在异步操作之后自动为你恢复当前的组件实例。
- 注释:等同于 async setup() 该组件在使用时必须由 Suspense 组件包裹
- VueUse:一个日益增长的 Vue 组合式函数集合。源代码本身就是一份不错的学习资料。
自定义指令
- 自定义指令主要是为了重用涉及普通元素的底层 DOM 访问的逻辑
- 一个自定义指令由一个包含类似组件生命周期钩子的对象来定义。钩子函数会接收到指令所绑定元素作为其参数
- 在 <script setup> 中,任何以 v 开头的驼峰式命名的变量都可以被用作一个自定义指令
<script setup>
// 在模板中启用 v-focus
const vFocus = {
mounted: (el) => el.focus()
}
</script>
<template>
<input v-focus />
</template>
- 将一个自定义指令全局注册到应用层级也是一种常见的做法
const app = createApp({})
// 使 v-focus 在所有组件中都可用
app.directive('focus', {
/* ... */
})
- 一个指令的定义对象可以提供几种钩子函数
const myDirective = {
// 在绑定元素的 attribute 前
// 或事件监听器应用前调用
created(el, binding, vnode, prevVnode) {
// 下面会介绍各个参数的细节
},
// 在元素被插入到 DOM 前调用
beforeMount(el, binding, vnode, prevVnode) {},
// 在绑定元素的父组件
// 及他自己的所有子节点都挂载完成后调用
mounted(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件更新前调用
beforeUpdate(el, binding, vnode, prevVnode) {},
// 在绑定元素的父组件
// 及他自己的所有子节点都更新后调用
updated(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件卸载前调用
beforeUnmount(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件卸载后调用
unmounted(el, binding, vnode, prevVnode) {}
}
- binding:一个对象,包含以下属性
- value:传递给指令的值。例如在 v-my-directive="1 + 1" 中,值是 2。
- oldValue:之前的值,仅在 beforeUpdate 和 updated 中可用。无论值是否更改,它都可用。
- arg:传递给指令的参数 (如果有的话)。例如在 v-my-directive:foo 中,参数是 "foo"。
- modifiers:一个包含修饰符的对象 (如果有的话)。例如在 v-my-directive.foo.bar 中,修饰符对象是 { foo: true, bar: true }。
- instance:使用该指令的组件实例。
- dir:指令的定义对象
- 注释:注册指令传递的包含各种钩子函数的对象
- 和内置指令类似,自定义指令的参数也可以是动态的
- 除了 el 外,其他参数都是只读的,不要更改它们。若你需要在不同的钩子间共享信息,推荐通过元素的 dataset attribute 实现
- 仅仅需要在 mounted 和 updated 上实现相同的行为,除此之外并不需要其他钩子。这种情况下我们可以直接用一个函数来定义指令
app.directive('color', (el, binding) => {
// 这会在 `mounted` 和 `updated` 时都调用
el.style.color = binding.value
})
- 和 attribute 不同,指令不能通过 v-bind="$attrs" 来传递给一个不同的元素。总的来说,不推荐在组件上使用自定义指令。
插件
- 插件 (Plugins) 是一种能为 Vue 添加全局功能的工具代码。下面是如何安装一个插件的示例
import { createApp } from 'vue'
const app = createApp({})
app.use(myPlugin, {
/* 可选的选项 */
})
- 一个插件可以是一个拥有 install() 方法的对象,也可以直接是一个安装函数本身。安装函数会接收到安装它的应用实例和传递给 app.use() 的额外选项作为参数
const myPlugin = {
install(app, options) {
// 配置此应用
}
}
- 插件发挥作用的常见场景主要包括以下几种:
- 通过 app.component() 和 app.directive() 注册一到多个全局组件或自定义指令。
- 通过 app.provide() 使一个资源可被注入进整个应用。
- 向 app.config.globalProperties 中添加一些全局实例属性或方法
Transition
- 可以将进入和离开动画应用到通过默认插槽传递给它的元素或组件上
- 进入或离开可以由以下的条件之一触发:
- 由 v-if 所触发的切换
- 由 v-show 所触发的切换
- 由特殊元素 <component> 切换的动态组件
- 改变特殊的 key 属性
- 注释:插入的组件在多个中进行切换,可以直接改变插入根元素的key属性来触发动画
- <Transition> 仅支持单个元素或组件作为其插槽内容。如果内容是一个组件,这个组件必须仅有一个根元素。
- 当一个 <Transition> 组件中的元素被插入或移除时,会发生下面这些事情:
- Vue 会自动检测目标元素是否应用了 CSS 过渡或动画。如果是,则一些 CSS 过渡 class 会在适当的时机被添加和移除。
- 注释:如果没有过渡或动画 class 不会被添加
- 注释:Vue 是通过监听目标元素的 transitionend 或 animationend 事件来检测目标元素是否应用了 CSS 过渡或动画的
- 注释:所以只是一些 CSS 过渡 class 会在适当的时机被添加和移除,其他一些 class 只要是插入 Transition 组件就会被添加
- 如果有作为监听器的 JavaScript 钩子,这些钩子函数会在适当时机被调用。
- 如果没有探测到 CSS 过渡或动画、也没有提供 JavaScript 钩子,那么 DOM 的插入、删除操作将在浏览器的下一个动画帧后执行。
- Vue 会自动检测目标元素是否应用了 CSS 过渡或动画。如果是,则一些 CSS 过渡 class 会在适当的时机被添加和移除。
- 一共有 6 个应用于进入与离开过渡效果的 CSS class
- v-enter-from:进入动画的起始状态。在元素插入之前添加,在元素插入完成后的下一帧移除。
- 注释:Vue 的 nextFrame 方法是基于浏览器的 requestAnimationFrame API 实现的,这个 API 可以让你在浏览器的下一个动画帧执行一些操作
- v-enter-active:进入动画的生效状态。应用于整个进入动画阶段。在元素被插入之前添加,在过渡或动画完成之后移除。这个 class 可以被用来定义进入动画的持续时间、延迟与速度曲线类型。
- v-enter-to:进入动画的结束状态。在元素插入完成后的下一帧被添加 (也就是 v-enter-from 被移除的同时),在过渡或动画完成之后移除。
- v-leave-from:离开动画的起始状态。在离开过渡效果被触发时立即添加,在一帧后被移除。
- v-leave-active:离开动画的生效状态。应用于整个离开动画阶段。在离开过渡效果被触发时立即添加,在过渡或动画完成之后移除。这个 class 可以被用来定义离开动画的持续时间、延迟与速度曲线类型。
- v-leave-to:离开动画的结束状态。在一个离开动画被触发后的下一帧被添加 (也就是 v-leave-from 被移除的同时),在过渡或动画完成之后移除。
- v-enter-from:进入动画的起始状态。在元素插入之前添加,在元素插入完成后的下一帧移除。
- 可以给 <Transition> 组件传一个 name prop 来声明一个过渡效果名:
- <Transition> 的 props (比如 name) 也可以是动态的
- 原生 CSS 动画和 CSS transition 的应用方式基本上是相同的,只有一点不同,那就是 *-enter-from 不是在元素插入后立即移除,而是在一个 animationend 事件触发时被移除
- 可以向 <Transition> 传递以下的 props 来指定自定义的过渡 class
- enter-from-class
- enter-active-class
- enter-to-class
- leave-from-class
- leave-active-class
- leave-to-class
- Vue 需要附加事件监听器,以便知道过渡何时结束。可以是 transitionend 或 animationend,这取决于你所应用的 CSS 规则。如果你仅仅使用二者的其中之一,Vue 可以自动探测到正确的类型。
- 此时你需要显式地传入 type prop 来声明,告诉 Vue 需要关心哪种类型,传入的值是 animation 或 transition
- 可以通过向 <Transition> 组件传入 duration prop 来显式指定过渡的持续时间 (以毫秒为单位)。总持续时间应该匹配延迟加上内部元素的过渡持续时间
- 注释:vue是通过嵌套根元素的 transitionend 或 animationend 事件来检测动画是否结束的,当动画不在根元素时可以通过传递过渡持续时间来控制
- 可以用对象的形式传入,分开指定进入和离开所需的时间
<Transition :duration="{ enter: 500, leave: 800 }">...</Transition>
- 可以通过监听 <Transition> 组件事件的方式在过渡过程中挂上钩子函数
<Transition
@before-enter="onBeforeEnter"
@enter="onEnter"
@after-enter="onAfterEnter"
@enter-cancelled="onEnterCancelled"
@before-leave="onBeforeLeave"
@leave="onLeave"
@after-leave="onAfterLeave"
@leave-cancelled="onLeaveCancelled"
>
<!-- ... -->
</Transition>
// 在元素被插入到 DOM 之前被调用
// 用这个来设置元素的 "enter-from" 状态
function onBeforeEnter(el) {}
// 在元素被插入到 DOM 之后的下一帧被调用
// 用这个来开始进入动画
function onEnter(el, done) {
// 调用回调函数 done 表示过渡结束
// 如果与 CSS 结合使用,则这个回调是可选参数
done()
}
// 当进入过渡完成时调用。
function onAfterEnter(el) {}
function onEnterCancelled(el) {}
// 在 leave 钩子之前调用
// 大多数时候,你应该只会用到 leave 钩子
function onBeforeLeave(el) {}
// 在离开过渡开始时调用
// 用这个来开始离开动画
function onLeave(el, done) {
// 调用回调函数 done 表示过渡结束
// 如果与 CSS 结合使用,则这个回调是可选参数
done()
}
// 在离开过渡完成、
// 且元素已从 DOM 中移除时调用
function onAfterLeave(el) {}
// 仅在 v-show 过渡中可用
function onLeaveCancelled(el) {}
- 在使用仅由 JavaScript 执行的动画时,最好是添加一个 :css="false" prop
- 如果你想在某个节点初次渲染时应用一个过渡效果,你可以添加 appear prop
- 很多情况下这可能并不符合需求。我们可能想要先执行离开动画,然后在其完成之后再执行元素的进入动画。手动编排这样的动画是非常复杂的,好在我们可以通过向 <Transition> 传入一个 mode prop 来实现这个行为
- 也支持 mode="in-out",虽然这并不常用。
<Transition mode="out-in">
...
</Transition>
TransitionGroup
- 默认情况下,它不会渲染一个容器元素。但你可以通过传入 tag prop 来指定一个元素作为容器元素来渲染。
- 过渡模式在这里不可用,因为我们不再是在互斥的元素之间进行切换
- 注释:过渡模式是指 model 这个 attribute 实现的先离开动画后进入动画的功能
- 列表中的每个元素都必须有一个独一无二的 key attribute
- 注释:通过 key 去判断哪个元素被删除、增加和移动的
- 当某一项被插入或移除时,它周围的元素会立即发生“跳跃”而不是平稳地移动。我们可以通过添加一些额外的 CSS 规则来解决这个问题:
.list-move, /* 对移动中的元素应用的过渡 */
.list-enter-active,
.list-leave-active {
transition: all 0.5s ease;
}
.list-enter-from,
.list-leave-to {
opacity: 0;
transform: translateX(30px);
}
/* 确保将离开的元素从布局流中删除
以便能够正确地计算移动的动画。 */
.list-leave-active {
position: absolute;
}
- 通过在 JavaScript 钩子中读取元素的 data attribute,我们可以实现带渐进延迟的列表动画
- 注释:通过元素的 data attribute 绑定每个元素特定的数据,然后在过渡方法中通过 el 获取,来为每个元素创建独立的效果
- 注释:
<TransitionGroup
tag="ul"
:css="false"
@before-enter="onBeforeEnter"
@enter="onEnter"
@leave="onLeave"
>
<li
v-for="(item, index) in computedList"
:key="item.msg"
:data-index="index"
>
{{ item.msg }}
</li>
</TransitionGroup>
function onEnter(el, done) {
gsap.to(el, {
opacity: 1,
height: '1.6em',
delay: el.dataset.index * 0.15,
onComplete: done
})
}
KeepAlive
- 注释:KeepAlive 保留了原生 dom 结构
- <KeepAlive> 默认会缓存内部的所有组件实例,但我们可以通过 include 和 exclude prop 来定制该行为
- 这两个 prop 的值都可以是一个以英文逗号分隔的字符串、一个正则表达式,或是包含这两种类型的一个数组:
<!-- 以英文逗号分隔的字符串 -->
<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>
- 在 3.2.34 或以上的版本中,使用 <script setup> 的单文件组件会自动根据文件名生成对应的 name 选项,无需再手动声明。
- onActivated 在组件挂载时也会调用,并且 onDeactivated 在组件卸载时也会调用
- 这两个钩子不仅适用于 <KeepAlive> 缓存的根组件,也适用于缓存树中的后代组件。
Teleport
- 将一个组件内部的一部分模板“传送”到该组件的 DOM 结构外层的位置去
- <Teleport> 接收一个 to prop 来指定传送的目标。to 的值可以是一个 CSS 选择器字符串,也可以是一个 DOM 元素对象。这段代码的作用就是告诉 Vue“把以下模板片段传送到 body 标签下”
<button @click="open = true">Open Modal</button>
<Teleport to="body">
<div v-if="open" class="modal">
<p>Hello from the modal!</p>
<button @click="open = false">Close</button>
</div>
</Teleport>
- <Teleport> 挂载时,传送的 to 目标必须已经存在于 DOM 中。
- 注释:
- 挂载时,Teleport组件会先创建一个占位符节点,用来标记传送内容的原始位置。
- 然后,Teleport组件会根据to属性找到目标节点,并将传送内容插入到目标节点的最后。
- 如果disabled属性为true,那么传送内容就不会插入到目标节点,而是保留在占位符节点的后面。
- 注释:
- 在某些场景下可能需要视情况禁用 <Teleport>。举例来说,我们想要在桌面端将一个组件当做浮层来渲染,但在移动端则当作行内组件。我们可以通过对 <Teleport> 动态地传入一个 disabled prop 来处理这两种不同情况。
- 这里的 isMobile 状态可以根据 CSS media query 的不同结果动态地更新
<Teleport :disabled="isMobile">
...
</Teleport>
Suspense
- 注释:setup 内允许使用 await,但在父组件中使用时必须由 Suspense 包裹
<Suspense>
└─ <Dashboard>
├─ <Profile>
│ └─ <FriendStatus>(组件有异步的 setup())
└─ <Content>
├─ <ActivityFeed> (异步组件)
└─ <Stats>(异步组件)
- 有了 <Suspense> 组件后,我们就可以在等待整个多层级组件树中的各个异步依赖获取结果时,在顶层展示出加载中或加载失败的状态
- 可以等待的异步依赖有两种
- 带有异步 setup() 钩子的组件。这也包含了使用 <script setup> 时有顶层 await 表达式的组件
- 异步组件
- 在这种情况下,加载状态是由 <Suspense> 控制,而该组件自己的加载、报错、延时和超时等选项都将被忽略
- <Suspense> 组件有两个插槽:#default 和 #fallback。两个插槽都只允许一个直接子节点。在可能的时候都将显示默认槽中的节点。否则将显示后备槽中的节点
注释:当异步依赖都完成时就会显示默认槽中的节点,否则就会显示后备槽 #fallback 中的节点
<Suspense>
<!-- 具有深层异步依赖的组件 -->
<Dashboard />
<!-- 在 #fallback 插槽中显示 “正在加载中” -->
<template #fallback>
Loading...
</template>
</Suspense>
- 在初始渲染时,<Suspense> 将在内存中渲染其默认的插槽内容。如果在这个过程中遇到任何异步依赖,则会进入挂起状态。在挂起状态期间,展示的是后备内容。当所有遇到的异步依赖都完成后,<Suspense> 会进入完成状态,并将展示出默认插槽的内容。
- 进入完成状态后,只有当默认插槽的根节点被替换时,<Suspense> 才会回到挂起状态。
- 发生回退时,后备内容不会立即展示出来。相反,<Suspense> 在等待新内容和异步依赖完成时,会展示之前 #default 插槽的内容。这个行为可以通过一个 timeout prop 进行配置:在等待渲染新内容耗时超过 timeout 之后,<Suspense> 将会切换为展示后备内容。若 timeout 值为 0 将导致在替换默认内容时立即显示后备内容。
- 组件会触发三个事件:pending、resolve 和 fallback。pending 事件是在进入挂起状态时触发。resolve 事件是在 default 插槽完成获取新内容时触发。fallback 事件则是在 fallback 插槽的内容显示时触发。
- 组件自身目前还不提供错误处理,不过你可以使用 errorCaptured 选项或者 onErrorCaptured() 钩子,在使用到 <Suspense> 的父组件中捕获和处理异步错误
- 注释:errorCaptured 选项是 Vue 2.5.0 新增的一个生命周期钩子,它可以让你在组件中捕获来自子孙组件的错误,并进行相应的处理
- 注释:当前组件 template 中的组件是当前组件的子组件,可以通过当前组件的 errorCaptured 选项捕获这些子组件的错误
- RouterView v-slot作用域插槽拿到的是被渲染的组件
- 常常会将 <Suspense> 和 <Transition>、<KeepAlive> 等组件结合
- 注释:KeepAlive组件允许并列插入多个动态组件,它们都会被缓存
<RouterView v-slot="{ Component }">
<template v-if="Component">
<Transition mode="out-in">
<KeepAlive>
<Suspense>
<!-- 主要内容 -->
<component :is="Component"></component>
<!-- 加载中状态 -->
<template #fallback>
正在加载...
</template>
</Suspense>
</KeepAlive>
</Transition>
</template>
</RouterView>
- Vue Router 使用动态导入对懒加载组件进行了内置支持。这些与异步组件不同,目前他们不会触发 <Suspense>。但是,它们仍然可以有异步组件作为后代,这些组件可以照常触发 <Suspense>。
- 注释:全局路由守卫 router.beforeEach 和 router.beforeResolve
单文件组件
- Vue SFC 是一个框架指定的文件格式,因此必须交由 @vue/compiler-sfc 编译为标准的 JavaScript 和 CSS,一个编译后的 SFC 是一个标准的 JavaScript(ES) 模块
工具链
- 若要了解如何为一个 Vite 项目配置 Vue 相关的特殊行为,比如向 Vue 编译器传递相关选项,请查看 @vitejs/plugin-vue 的文档。
- 从 Vue CLI 迁移到 Vite 的资源
- 前缀为 vue.runtime.* 的文件是只包含运行时的版本:不包含编译器,当使用这个版本时,所有的模板都必须由构建步骤预先编译。
- 如果因为某些原因,在有构建步骤时,你仍需要浏览器内的模板编译,你可以更改构建工具配置,将 vue 改为相应的版本 vue/dist/vue.esm-bundler.js
- 推荐使用的 IDE 是 VSCode,配合 Vue 语言特性 (Volar) 插件。该插件提供了语法高亮、TypeScript 支持,以及模板内表达式与组件 props 的智能提示
- Vue 的浏览器开发者插件使我们可以浏览一个 Vue 应用的组件树,查看各个组件的状态,追踪状态管理的事件,还可以进行组件性能分析。
- 使用 vue-tsc 可以在命令行中执行相同的类型检查,通常用来生成单文件组件的 d.ts 文件。
- Cypress 推荐用于 E2E 测试。也可以通过 Cypress 组件测试运行器来给 Vue SFC 作单文件组件测试。
- 注释:ESE 模仿用户对客户端进行测试
- Vitest 是一个追求更快运行速度的测试运行器,由 Vue / Vite 团队成员开发。主要针对基于 Vite 的应用设计,可以为组件提供即时响应的测试反馈。
- 注释:即时响应的测试反馈是指,当你修改了组件的代码或者测试用例时,Vitest 可以立即重新运行相关的测试,并显示测试结果和覆盖率。这样,你就可以快速地检查你的代码是否符合预期,而不需要等待传统的测试运行器重新编译和执行整个测试套件。
- Jest 可以通过 vite-jest 配合 Vite 使用。不过只推荐在你已经有一套基于 Jest 的测试集、且想要迁移到基于 Vite 的开发配置时使用,因为 Vitest 也能够提供类似的功能,且后者与 Vite 的集成更方便高效。
- 启用类似 lint-staged 一类的工具在 git commit 提交时自动执行规范检查。
- Prettier 也提供了内置的 Vue SFC 格式化支持
- 自定义块被编译成导入到同一 Vue 文件的不同请求查询。这取决于底层构建工具如何处理这类导入请求。
- 如果使用 Vite,需使用一个自定义 Vite 插件将自定义块转换为可执行的 JavaScript 代码。[例子](https://github.com/vitejs/vite-plugin-vue/tree/main/packages/plugin-vue#example-for-transforming-custom-blocks)
- 如果使用 Vue CLI 或只是 webpack,需要使用一个 loader 来配置如何转换匹配到的自定义块。[例子](https://vue-loader.vuejs.org/zh/guide/custom-blocks.html)
- @vue/compiler-sfc
- [文档](https://github.com/vuejs/core/tree/main/packages/compiler-sfc)
- 这个包是 Vue 核心 monorepo 的一部分,并始终和 vue 主包版本号保持一致。
- 注释:monorepo 是指一个仓库管理多个项目的代码
- 请始终选择通过 vue/compiler-sfc 的深度导入来使用这个包,因为这样可以确保其与 Vue 运行时版本同步。
- @vitejs/plugin-vue
- [文档](https://github.com/vitejs/vite-plugin-vue/tree/main/packages/plugin-vue)
- 为 Vite 提供 Vue SFC 支持的官方插件。
- vue-loader
- [文档](https://vue-loader.vuejs.org/zh/)
- 如何在 Vue CLI 中更改 vue-loader 选项的文档。
- 其他在线演练场
状态管理
- 为了确保改变状态的逻辑像状态本身一样集中,建议在 store 上定义方法,方法的名称应该要能表达出行动的意图:
// store.js
import { reactive } from 'vue'
export const store = reactive({
count: 0,
increment() {
this.count++
}
})
- 请注意这里点击的处理函数使用了 store.increment(),带上了圆括号作为内联表达式调用,因为它并不是组件的方法,并且必须要以正确的 this 上下文来调用。
<template>
<button @click="store.increment()">
From B: {{ store.count }}
</button>
</template>
- Pinia
- 在大规模的生产应用中还有很多其他事项需要考虑:
- 与 Vue DevTools 集成,包括时间轴、组件内部审查和时间旅行调试
- 注释:时间轴:这是一个显示你的应用程序中发生的各种事件和活动的图形界面,你可以在时间轴上查看和过滤不同类型的事件,如组件更新、用户交互、路由变化、性能指标等。你也可以在时间轴上选择某个事件,查看它发生时的应用程序状态和快照
- 注释:组件内部审查,就是查看组件树和属性
- 注释:时间旅行调试:这是一个让你在时间上前进或后退,重现或回溯你的应用程序状态的工具,你可以使用暂停、播放、快进、快退等按钮,控制应用程序的执行流程。你也可以使用滑块或输入框,跳转到某个特定的时间点
- 模块热更新 (HMR)
- 服务端渲染支持
- 相比于 Vuex,Pinia 提供了更简洁直接的 API,并提供了组合式风格的 API,最重要的是,在使用 TypeScript 时它提供了更完善的类型推导
测试
- 当设计你的 Vue 应用的测试策略时,你应该利用以下几种测试类型:
- 单元测试:检查给定函数、类或组合式函数的输入是否产生预期的输出或副作用。
- 组件测试:检查你的组件是否正常挂载和渲染、是否可以与之互动,以及表现是否符合预期。这些测试比单元测试导入了更多的代码,更复杂,需要更多时间来执行。
- 端到端测试:检查跨越多个页面的功能,并对生产构建的 Vue 应用进行实际的网络请求。这些测试通常涉及到建立一个数据库或其他后端。
vue3 3.3.4的更多相关文章
- vue3+typescript引入外部文件
vue3+typescript中引入外部文件有几种方法 (eg:引入echarts) 第一种方法: 1 indext.html中用script引入 <div id="app" ...
- 预计2019年发布的Vue3.0到底有什么不一样的地方?
摘要: Vue 3.0预览. 原文:预计今年发布的Vue3.0到底有什么不一样的地方? 作者:小肆 微信公众号:技术放肆聊 Fundebug经授权转载,版权归原作者所有. 还有几个月距离 vue2 的 ...
- 纯小白入手 vue3.0 CLI - 3.3 - 路由的导航守卫
vue3.0 CLI 真小白一步一步入手全教程系列:https://www.cnblogs.com/ndos/category/1295752.html 尽量把纷繁的知识,肢解重组成为可以堆砌的知识. ...
- 纯小白入手 vue3.0 CLI - 3.2 - 路由的初级使用
vue3.0 CLI 真小白一步一步入手全教程系列:https://www.cnblogs.com/ndos/category/1295752.html 尽量把纷繁的知识,肢解重组成为可以堆砌的知识. ...
- 纯小白入手 vue3.0 CLI - 3.1 - 路由 ( router )
vue3.0 CLI 真小白一步一步入手全教程系列:https://www.cnblogs.com/ndos/category/1295752.html 尽量把纷繁的知识,肢解重组成为可以堆砌的知识. ...
- 纯小白入手 vue3.0 CLI - 2.7 - 组件之间的数据流
vue3.0 CLI 真小白一步一步入手全教程系列:https://www.cnblogs.com/ndos/category/1295752.html 尽量把纷繁的知识,肢解重组成为可以堆砌的知识. ...
- VUE3.0升级与配置(跨域、全局scss变量等)
1.检查本机vue版本 vue -V 2.升级vue3.0命令 npm install -g @vue/cli 3.创建完项目后,在项目根目录新增vue.config.js文件,插入代码(简洁) mo ...
- 使用vue3.0和element实现后台管理模板
通过自己所学的这段时间,利用空余时间,使用vue3.0脚手架搭建的一个关于后台的管理模板,所实现功能也是模仿一个后台的界面,数据分为两种存放,一种是直接存储到mlab,这里的数据是存放这登录注册,只有 ...
- 如何启动一个Vue3.x项目
1. 安装node.js 2. cd到项目目录下 3. npm run serve Node.js下载与安装(npm) Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运 ...
- 纯小白入手 vue3.0 CLI - 2.6 - 组件的复用
vue3.0 CLI 真小白一步一步入手全教程系列:https://www.cnblogs.com/ndos/category/1295752.html 我的 github 地址 - vue3.0St ...
随机推荐
- [oeasy]python0051_ 转义_escape_字符_character_单引号_双引号_反引号_ 退格键
转义字符 回忆上次内容 上次研究的是进制转化 10进制可以转化为其他形式 bin oct hex 其他进制也可以转化为10进制 int 可以设置base来决定转为多少进制 回忆一下 我们为什么会有八进 ...
- oeasy教您玩转vim - 71 - # 视图view
视图view 回忆上次折叠的细节 折叠方式很多 我们一般就用默认的就行 indent 很好用 前提是缩进语法严格到位 这样语法和排版都能同时确保 打开关闭 zc.zo 是打开关闭当前行 zm.zr ...
- 【漏洞分析】Li.Fi攻击事件分析:缺乏关键参数检查的钻石协议
背景信息 2024 年 7 月 16日,Li.Fi 协议遭受黑客攻击,漏洞成因是钻石协议中 diamond 合约新添加的 facet 合约没有对参数进行检查,导致 call 函数任意执行.且 diam ...
- [rCore学习笔记 012]彩色化LOG
实验要求 实现分支:ch1 完成实验指导书中的内容并在裸机上实现 hello world 输出. 实现彩色输出宏(只要求可以彩色输出,不要求 log 等级控制,不要求多种颜色) 隐形要求 可以关闭内核 ...
- RESTful服务与swagger
一开始刚学springboot的时候 restful服务+swagger一点都看不懂,现在知识学了一些,再回头看这些东西就简单很多了. 自己跟视频做了一个零件项目,里面写了一些零零散散的模块,其中在视 ...
- 【Java】讲讲StreamAPI
预设场景: 从Mybatis调用Mapper得到的用户集合 List<UserDTO> userList = new ArrayList<>(); 常用的几种API用法示例: ...
- 【Spring-Security】Re05 权限控制及403处理
一.访问控制方法及控制项: 上述配置中的URL后面都离不开的一个访问控制抉择: 1.全部允许 PermiAll 2.全部拒绝 DenyAll 3.允许匿名访问 Anonymous 也就是普通访问者 4 ...
- 【开启报名】同学看过来,Apache DolphinScheduler开源之夏课题任务正式发布!
如果你还拥有着一张有效的"学生证",在这个充满机遇的夏天,我们诚邀你加入一个充满挑战和机遇的开源冒险--开源之夏. 这不仅是一个简单的编程开发活动,假如你成功参加并结项之后,还能获 ...
- Digest Auth 摘要认证
1.该代码展示了使用Apache HttpClient库进行HTTP请求,并处理基于MD5的HTTP Digest认证的过程. Digests类实现了MD5加密算法,HttpUtils类处理了GET. ...
- mybatis 中 实体类字段为 month SQL 会报错的问题
因为 month 是 mysql 的关键字 ,所以 你的实体类字段改成 months months months months months months就行了