PS:自己记录用 看过官网的可忽略哦
创建一个应用实例
const app = Vue.createApp({
/* 选项 */
})
在应用中注册“全局”组件
const app = Vue.createApp({})
app.component('SearchInput', SearchInputComponent)
app.directive('focus', FocusDirective)
app.use(LocalePlugin)
应用实例暴露的大多数方法都会返回该同一实例,因此允许链式
Vue.createApp({})
.component('SearchInput', SearchInputComponent)
.directive('focus', FocusDirective)
.use(LocalePlugin)
根组件
传递给
createApp
的选项用于配置根组件。当我们挂载应用时,该组件被用作渲染的起点把一个 Vue 应用挂载到
<div id="app"></div>
,应该传入#app
:
const RootComponent = {
/* 选项 */
}
const app = Vue.createApp(RootComponent)
const vm = app.mount('#app')
mount
不返回应用本身,返回的是根组件实例
v-once 指令
执行一次性地插值,当数据改变时,插值处的内容不会更新
这会影响到该节点上的其它数据绑定
在 Vue 中渲染纯 HTML 元素的速度非常快,但有时你可能有一个包含很多静态内容的组件。在这些情况下,可以通过向根元素添加
v-once
指令来确保只对其求值一次,然后进行缓存
<span v-once>这个将不会改变: {{ msg }}</span>
Vue.extend(options):创建一个Vue
的子类并且返回相应的 constructor
options
:组件选项
使用时需要 new
详见:
vue api
中的Vue.extend
及:
vue组件的三种用法
中的构造组件+Vue.prototype
使用基础
Vue
构造器,创建一个“子类”。参数是一个包含组件选项的对象。
data
选项是特例,在 Vue.extend() 中它必须是函数
<div id="mount-point"></div>
// 创建构造器
var Profile = Vue.extend({
template: '<p>{{firstName}} {{lastName}} aka {{alias}}</p>',
data: function () {
return {
firstName: 'Walter',
lastName: 'White',
alias: 'Heisenberg'
}
}
})
// 创建 Profile 实例,并挂载到一个元素上。
new Profile().$mount('#mount-point')
<!-- 结果 -->
<!-- <div id="mount-point"></div>被p标签替换掉了 -->
<p>Walter White aka Heisenberg</p>
构建版本术语
1、完整版:
同时包含编译器和运行时的版本。
2、编译器:
用来
将模板字符串
编译成为JavaScript
渲染函数的代码。
3、运行时:
用来创建
Vue
实例、渲染并处理虚拟DOM
等的代码。基本上就是除去编译器的其它一切。
如果需要在客户端编译模板
(比如传入一个字符串给 template
选项,或挂载到一个元素上并以其 DOM
内部的 HTML
作为模板),就将需要加上编译器,即完整版:
在vue.config.js中设置runtimeCompiler: true
// 需要编译器
new Vue({ template: '<div>{{ hi }}</div>' })
// 不需要编译器
new Vue({ render (h) { return h('div', this.hi) } })
指令
动态参数
可以用方括号括起来的 JavaScript 表达式作为一个指令的参数
动态绑定属性
<!-- 注意,参数表达式的写法存在一些约束,如之后的“对动态参数表达式的约束”章节所述。 -->
<a v-bind:[attributeName]="url"> ... </a>
当 attributeName 其值为 "href"
,那么这个绑定将等价于 v-bind:href
。
动态绑定方法
<a v-on:[eventName]="doSomething"> ... </a>
当 eventName
的值为 "focus"
时,v-on:[eventName]
将等价于 v-on:focus
注意事项
空格和引号,放在 HTML attribute 名里是无效的
<!-- 这会触发一个编译警告 -->
<a v-bind:['foo' + bar]="value"> ... </a>
解决方法
使用没有空格或引号的表达式,或用计算属性替代这种复杂表达式
避免使用大写字符来命名键名
在 DOM 中使用模板时 (直接在一个 HTML 文件里撰写模板),还需要避免使用大写字符来命名键名,因为浏览器会把 attribute 名全部强制转为小写:
<!-- 在 DOM 中使用模板时这段代码会被转换为 `v-bind:[someattr]`。 除非在实例中有一个名为“someattr”的 property,否则代码不会工作。 -->
<a v-bind:[someAttr]="value"> ... </a>
计算属性
计算属性缓存 vs 方法
计算属性是基于它们的响应式依赖进行缓存的。只在相关响应式依赖发生改变时它们才会重新求值。这就意味着只要
message
还没有发生改变,多次访问reversedMessage
计算属性会立即返回之前的计算结果,而不必再次执行函数
计算属性写法
<div id="example">
<p>Original message: "{{ message }}"</p>
<p>Computed reversed message: "{{ reversedMessage }}"</p>
</div>
computed: {
// 计算属性的 getter
reversedMessage: function () {
// `this` 指向 vm 实例
return this.message.split('').reverse().join('')
}
}
方法写法
<p>Reversed message: "{{ reversedMessage() }}"</p>
// 在组件中
methods: {
reversedMessage: function () {
return this.message.split('').reverse().join('')
}
}
不是响应式的数据不会触发重新计算
下面的计算属性将不再更新,因为
Date.now()
不是响应式依赖但是如果写成方法 每当触发重新渲染时,调用方法将总会再次执行函数
computed: { now: function () { return Date.now() } }
计算属性 vs 侦听属性
Vue 提供了一种更通用的方式来观察和响应 Vue 实例上的数据变动:侦听属性。
当你有一些数据需要随着其它数据变动而变动时,很容易滥用
watch
。然而,通常更好的做法是使用计算属性而不是命令式的
watch
回调
计算属性的 setter
// ...
computed: {
fullName: {
// getter
get: function () {
return this.firstName + ' ' + this.lastName
},
// setter
set: function (newValue) {
var names = newValue.split(' ')
this.firstName = names[0]
this.lastName = names[names.length - 1]
}
}
}
// ...
侦听器
需要在
数据变化时
执行异步
或开销较大的操作
时,这个方式是最有用的
<div id="watch-example">
<p> Ask a yes/no question:
<input v-model="question">
</p>
<p>{{ answer }}</p>
</div>
<!-- 因为 AJAX 库和通用工具的生态已经相当丰富,Vue 核心代码没有重复 -->
<!-- 提供这些功能以保持精简。这也可以让你自由选择自己更熟悉的工具。 -->
<script>
var watchExampleVM = new Vue({
el: '#watch-example',
data: {
question: '',
answer: 'I cannot give you an answer until you ask a question!'
},
watch: {
// 如果 `question` 发生改变,这个函数就会运行
question: function (newQuestion, oldQuestion) {
this.answer = 'Waiting for you to stop typing...'
this.debouncedGetAnswer()
}
},
created: function () {
// `_.debounce` 是一个通过 Lodash 限制操作频率的函数。
// 在这个例子中,我们希望限制访问 yesno.wtf/api 的频率
// AJAX 请求直到用户输入完毕才会发出。想要了解更多关于
// `_.debounce` 函数 (及其近亲 `_.throttle`) 的知识,
// 请参考:https://lodash.com/docs#debounce
this.debouncedGetAnswer = _.debounce(this.getAnswer, 500)
},
methods: {
getAnswer: function () {
if (this.question.indexOf('?') === -1) {
this.answer = 'Questions usually contain a question mark. ;-)'
return
}
this.answer = 'Thinking...'
var vm = this
axios.get('https://yesno.wtf/api')
.then(function (response) {
vm.answer = _.capitalize(response.data.answer)
})
.catch(function (error) {
vm.answer = 'Error! Could not reach the API. ' + error
})
}
}
})
</script>
用 key
管理可复用的元素
Vue 会尽可能高效地渲染元素,通常会复用已有元素而不是从头开始渲染。这么做除了使 Vue 变得非常快之外,还有其它一些好处
示例:允许用户在不同的登录方式之间切换
<template v-if="loginType === 'username'">
<label>Username</label>
<input placeholder="Enter your username">
</template>
<template v-else>
<label>Email</label>
<input placeholder="Enter your email address">
</template>
上面的代码中切换
loginType
将不会清除用户已经输入的内容。因为两个模板使用了相同的元素,<input>
不会被替换掉——仅仅是替换了它的placeholder
Vue 为你提供了一种方式来表达“这两个元素是完全独立的,不要复用它们”。只需添加一个具有唯一值的 key
attribute 即可
<template v-if="loginType === 'username'">
<label>Username</label>
<input placeholder="Enter your username" key="username-input">
</template>
<template v-else>
<label>Email</label>
<input placeholder="Enter your email address" key="email-input">
</template>
v-if
vs v-show
v-if
是“真正”的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。
v-if
也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。
相比之下,v-show
就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 进行切换。
一般来说,v-if
有更高的切换开销,而 v-show
有更高的初始渲染开销。因此,如果需要非常频繁地切换,则使用 v-show
较好;如果在运行时条件很少改变,则使用 v-if
较好。
v-for 的 key
当 Vue 正在更新使用
v-for
渲染的元素列表时,它默认使用“就地更新”的策略。如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序,而是就地更新每个元素,并且确保它们在每个索引位置正确渲染。
这个默认的模式是高效的,但是只适用于不依赖子组件状态或临时 DOM 状态 (例如:表单输入值) 的列表渲染输出
为了给 Vue 一个提示,以便它能跟踪每个节点的身份,从而重用和重新排序现有元素,需要为每项提供一个唯一
key
v-for
与 v-if
一同使用
不推荐在同一元素上使用
v-if
和v-for
当它们处于同一节点,
v-for
的优先级比v-if
更高,这意味着v-if
将分别重复运行于每个v-for
循环中
事件修饰符
<!-- 阻止单击事件继续传播 -->
<a v-on:click.stop="doThis"></a>
<!-- 提交事件不再重载页面 -->
<form v-on:submit.prevent="onSubmit"></form>
<!-- 修饰符可以串联 -->
<a v-on:click.stop.prevent="doThat"></a>
<!-- 只有修饰符 -->
<form v-on:submit.prevent></form>
<!-- 添加事件监听器时使用事件捕获模式 -->
<!-- 即内部元素触发的事件先在此处理,然后才交由内部元素进行处理 -->
<div v-on:click.capture="doThis">...</div>
<!-- 只当在 event.target 是当前元素自身时触发处理函数 -->
<!-- 即事件不是从内部元素触发的 -->
<div v-on:click.self="doThat">...</div>
.exact
修饰符
允许你控制由精确的系统修饰符组合触发的事件
<!-- 即使 Alt 或 Shift 被一同按下时也会触发 -->
<button @click.ctrl="onClick">A</button>
<!-- 有且只有 Ctrl 被按下的时候才触发 -->
<button @click.ctrl.exact="onCtrlClick">A</button>
<!-- 没有任何系统修饰符被按下的时候才触发 -->
<button @click.exact="onClick">A</button>
表单输入
修饰符
.lazy
在 change
事件_之后_进行同步
在默认情况下,
v-model
在每次input
事件触发后将输入框的值与数据进行同步 (除了上述输入法组织文字时)。你可以添加lazy
修饰符,从而转为在change
事件_之后_进行同步
<!-- 在“change”时而非“input”时更新 -->
<input v-model.lazy="msg" />
.number
将用户的输入值转为数值类型
<input v-model.number="age" type="number" />
.trim
自动过滤用户输入的首尾空白字符
<input v-model.trim="msg" />
组件
// 创建应用
const app = Vue.createApp({})
// 定义组件注册
app.component('button-counter', {
data() {
return {
count: 0
}
},
template: `
<button v-on:click="count++">
You clicked me {{ count }} times.
</button>`
})
// 挂在实例
app.mount('#components-demo')
<div id="components-demo">
// 把这个组件作为一个根实例中的自定义元素来使用
<button-counter></button-counter>
</div>
组件注册
app.component
全局注册
const app = Vue.createApp({})
app.component('component-a', {
/* ... */
})
app.component('component-b', {
/* ... */
})
app.component('component-c', {
/* ... */
})
app.mount('#app')
局部注册
通过一个普通的 JavaScript 对象来定义组件:
const ComponentA = {
/* ... */
}
const ComponentB = {
/* ... */
}
const ComponentC = {
/* ... */
}
然后在 components
选项中定义你想要使用的组件:
const app = Vue.createApp({
components: {
'component-a': ComponentA,
'component-b': ComponentB
}
})
局部注册的组件在其子组件中不可用
如果你希望 ComponentA
在 ComponentB
中可用,则你需要这样写:
const ComponentA = {
/* ... */
}
const ComponentB = {
components: {
'component-a': ComponentA
}
// ...
}
通过 Babel 和 webpack 使用 ES2015 模块
import ComponentA from './ComponentA.vue'
export default {
components: {
ComponentA
}
// ...
}
在模块系统中局部注册
import ComponentA from './ComponentA'
import ComponentC from './ComponentC'
export default {
components: {
ComponentA,
ComponentC
}
// ...
}
监听子组件事件
父组件 监听器
<blog-post ... @enlarge-text="postFontSize += 0.1"></blog-post>
子组件
<button @click="$emit('enlargeText', 0.1)">
Enlarge text
</button>
可以在组件的 emits
选项中列出已抛出的事件
app.component('blog-post', {
props: ['title'],
emits: ['enlargeText']
})
验证抛出的事件
如果使用对象语法而不是数组语法定义发出的事件,则可以验证它
要添加验证,将为事件分配一个函数,该函数接收传递给
$emit
调用的参数,并返回一个布尔值以指示事件是否有效。
app.component('custom-form', {
emits: {
// 没有验证
click: null,
// 验证submit 事件
submit: ({ email, password }) => {
if (email && password) {
return true
} else {
console.warn('Invalid submit event payload!')
return false
}
}
},
methods: {
submitForm(email, password) {
this.$emit('submit', { email, password })
}
}
})
在组件上使用 v-model
<input v-model="searchText" />
等价于
<input :value="searchText" @input="searchText = $event.target.value" />
当用在组件上时,v-model
则会这样:
<custom-input
:model-value="searchText"
@update:model-value="searchText = $event"
></custom-input>
- 将其
value
attribute 绑定到一个名叫modelValue
的 prop 上 - 在其
input
事件被触发时,将新的值通过自定义的update:modelValue
事件抛出 写成代码之后是这样的:
app.component('custom-input', {
props: ['modelValue'],
emits: ['update:modelValue'],
template: `
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
>
`
})
现在 v-model
就应该可以在这个组件上完美地工作起来了:
<custom-input v-model="searchText"></custom-input>
实现 v-model
的另一种方法是使用 computed
app.component('custom-input', {
props: ['modelValue'],
emits: ['update:modelValue'],
template: `
<input v-model="value">
`,
computed: {
value: {
get() {
return this.modelValue
},
set(value) {
this.$emit('update:modelValue', value)
}
}
}
})
解析 DOM 模板时的注意事项
元素放置限制
有些 HTML 元素,诸如 <ul>
、<ol>
、<table>
和 <select>
,对于哪些元素可以出现在其内部是有严格限制的。而有些元素,诸如 <li>
、<tr>
和 <option>
,只能出现在其它某些特定的元素内部。
table>
<blog-post-row></blog-post-row>
</table>
这个自定义组件 <blog-post-row>
会被作为无效的内容提升到外部,并导致最终渲染结果出错。我们可以使用特殊的 is
attribute 作为一个变通的办法:
<table>
<tr is="vue:blog-post-row"></tr>
</table>
当它用于原生 HTML 元素时,
is
的值必须以vue:
开头,才可以被解释为 Vue 组件。这是避免和原生自定义元素混淆。
不区分大小写
HTML attribute 名不区分大小写,因此浏览器将所有大写字符解释为小写。这意味着当你在 DOM 模板中使用时,驼峰 prop 名称和 event 处理器参数需要使用它们的 kebab-cased (横线字符分隔) 等效值:
// 在JavaScript中的驼峰
app.component('blog-post', {
props: ['postTitle'],
template: `
<h3>{{ postTitle }}</h3>
`
})
<!-- 在HTML则是横线字符分割 -->
<blog-post post-title="hello!"></blog-post>
插槽
Vue 实现了一套内容分发的 API,这套 API 的设计灵感源自 Web Components 规范草案,将
<slot>
元素作为承载分发内容的出口
<todo-button>
Add todo
</todo-button>
然后在 <todo-button>
的模板中,你可能有:
<!-- todo-button 组件模板 -->
<button class="btn-primary">
<slot></slot>
</button>
当组件渲染的时候,<slot></slot>
将会被替换为“Add Todo”
<!-- 渲染 HTML -->
<button class="btn-primary">
Add todo
</button>
插槽还可以包含任何模板代码,包括 HTML\其他组件
如果
<todo-button>
的 template 中没有包含一个<slot>
元素,则该组件起始标签和结束标签之间的任何内容都会被抛弃
渲染作用域
插槽不能访问
<todo-button>
的作用域(不能在父组件中访问子组件的数据)
父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的
备用内容:在没有提供内容的时候被渲染
<button type="submit">
<slot>Submit</slot>
</button>
具名插槽
需要多个插槽
<div class="container">
<header>
<!-- 我们希望把页头放这里 -->
</header>
<main>
<!-- 我们希望把主要内容放这里 -->
</main>
<footer>
<!-- 我们希望把页脚放这里 -->
</footer>
</div>
(子组件接受内容)对于这样的情况,<slot>
元素有一个特殊的 attribute:name
。这个 attribute 可以用来定义额外的插槽:
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
不带
name
的<slot>
出口会带有隐含的名字“default”
(父组件传递内容)在向具名插槽提供内容的时候,我们可以在一个 <template>
元素上使用 v-slot
指令,并以 v-slot
的参数的形式提供其名称
<base-layout>
<template v-slot:header>
<h1>Here might be a page title</h1>
</template>
<template v-slot:default>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
</template>
<template v-slot:footer>
<p>Here's some contact info</p>
</template>
</base-layout>
结果
<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>
作用域插槽
有时让插槽内容能够访问子组件中才有的数据是很有用的。
我们有一个组件,包含 todo-items 的列表
app.component('todo-list', {
data() {
return {
items: ['Feed a cat', 'Buy milk']
}
},
template: `
<ul>
<li v-for="(item, index) in items">
{{ item }}
</li>
</ul>
`
})
想把 {{ item }}
替换为 <slot>
,以便在父组件上自定义
todo-list 的父组件 想获取到item
<todo-list>
<i class="fas fa-check"></i>
<span class="green">{{ item }}</span>
</todo-list>
要使 item
可用于父级提供的插槽内容,我们可以添加一个 <slot>
元素并将其绑定为属性
todo-list 组件
<ul>
<li v-for="( item, index ) in items">
<slot :item="item"></slot>
</li>
</ul>
根据自己的需要将很多的 attribute 绑定到 slot
上
<ul>
<li v-for="( item, index ) in items">
<slot :item="item" :index="index" :another-attribute="anotherAttribute"></slot>
</li>
</ul>
绑定在 <slot>
元素上的 attribute 被称为插槽 prop。现在在父级作用域中,我们可以使用带值的 v-slot
来定义我们提供的插槽 prop 的名字:
todo-list 的父组件 想获取到item
<todo-list>
<template v-slot:default="slotProps">
<i class="fas fa-check"></i>
<span class="green">{{ slotProps.item }}</span>
</template>
</todo-list>
独占默认插槽的缩写语法
在上述情况下,当被提供的内容只有默认插槽时,组件的标签才可以被当作插槽的模板来使用。这样我们就可以把 v-slot
直接用在组件上:
<todo-list v-slot:default="slotProps">
<i class="fas fa-check"></i>
<span class="green">{{ slotProps.item }}</span>
</todo-list>
这种写法还可以更简单。就像假定未指明的内容对应默认插槽一样,不带参数的 v-slot
被假定对应默认插槽:
<todo-list v-slot="slotProps">
<i class="fas fa-check"></i>
<span class="green">{{ slotProps.item }}</span>
</todo-list>
默认插槽的缩写语法不能和具名插槽混用,因为它会导致作用域不明确:
<!-- 无效,会导致警告 -->
<todo-list v-slot="slotProps">
<i class="fas fa-check"></i>
<span class="green">{{ slotProps.item }}</span>
<template v-slot:other="otherSlotProps">
slotProps is NOT available here
</template>
</todo-list>
只要出现多个插槽,请始终为所有的插槽使用完整的基于 <template>
的语法:
<todo-list>
<template v-slot:default="slotProps">
<i class="fas fa-check"></i>
<span class="green">{{ slotProps.item }}</span>
</template>
<template v-slot:other="otherSlotProps">
...
</template>
</todo-list>
解构插槽 Prop
作用域插槽的内部工作原理是将你的插槽内容包括在一个传入单个参数的函数里:
function (slotProps) {
// ... 插槽内容 ...
}
这意味着 v-slot
的值实际上可以是任何能够作为函数定义中的参数的 JavaScript 表达式。你也可以使用 ES2015 解构来传入具体的插槽 prop,如下
<todo-list v-slot="{ item }">
<i class="fas fa-check"></i>
<span class="green">{{ item }}</span>
</todo-list>
将 item
重命名为 todo
:
<todo-list v-slot="{ item: todo }">
<i class="fas fa-check"></i>
<span class="green">{{ todo }}</span>
</todo-list>
甚至可以定义备用内容,用于插槽 prop 是 undefined 的情形
<todo-list v-slot="{ item = 'Placeholder' }">
<i class="fas fa-check"></i>
<span class="green">{{ item }}</span>
</todo-list>
动态插槽名
可以像动态指令参数一样用在 v-slot
上,来定义动态的插槽名:
<base-layout>
<template v-slot:[dynamicSlotName]>
...
</template>
</base-layout>
具名插槽的缩写
把参数之前的所有内容 (v-slot:
) 替换为字符 #
<base-layout>
<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>
</base-layout>
Provide / Inject
无论组件层次结构有多深,父组件都可以作为其所有子组件的依赖提供者。这个特性有两个部分:父组件有一个 provide
选项来提供数据,子组件有一个 inject
选项来开始使用这些数据。
provide
是一个返回对象的函数
- 父组件不需要知道哪些子组件使用它 provide 的 property
- 子组件不需要知道 inject 的 property 来自哪里
app.component('todo-list', {
data() {
return {
todos: ['Feed a cat', 'Buy tickets']
}
},
provide() {
return {
todoLength: this.todos.length
}
},
template: `
...
`
})
更改了 todos
的列表,这个变化并不会反映在 inject 的 todoLength
property 中。这是因为默认情况下,provide/inject
绑定并不是响应式的。可以通过传递一个 ref
property 或 reactive
对象给 provide
来改变这种行为
为 provide 的
todoLength
分配一个组合式 APIcomputed
property:
app.component('todo-list', {
// ...
provide() {
return {
todoLength: Vue.computed(() => this.todos.length)
}
}
})
app.component('todo-list-statistics', {
inject: ['todoLength'],
created() {
console.log(`Injected property: ${this.todoLength.value}`) // > Injected property: 5
}
})
动态组件 & 异步组件
在动态组件上使用 keep-alive
组件之间切换的时候,保持这些组件的状态,以避免反复重渲染导致的性能问题
<!-- 失活的组件将会被缓存!-->
<keep-alive>
<component :is="currentTabComponent"></component>
</keep-alive>
异步组件defineAsyncComponent
将应用分割成小一些的代码块,并且只在需要的时候才从服务器加载一个模块。为了简化,Vue 有一个
defineAsyncComponent
方法:
const { createApp, defineAsyncComponent } = Vue
const app = createApp({})
const AsyncComp = defineAsyncComponent(
() =>
new Promise((resolve, reject) => {
resolve({
template: '<div>I am async!</div>'
})
})
)
app.component('async-example', AsyncComp)
此方法接受返回 Promise
的工厂函数。从服务器检索组件定义后,应调用 Promise 的 resolve
回调。你也可以调用 reject(reason)
,来表示加载失败。
以在工厂函数中返回一个
Promise
,把 webpack 2 和 ES2015 语法相结合后,可以这样使用动态地导入:
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent(() =>
import('./components/AsyncComponent.vue')
)
app.component('async-component', AsyncComp)
在局部注册组件时,也可以使用 defineAsyncComponent
import { createApp, defineAsyncComponent } from 'vue'
createApp({
// ...
components: {
AsyncComponent: defineAsyncComponent(() =>
import('./components/AsyncComponent.vue')
)
}
})
与 Suspense 一起使用
异步组件在默认情况下是可挂起的。这意味着如果它在父链中有一个
<Suspense>
,它将被视为该<Suspense>
的异步依赖。在这种情况下,加载状态将由<Suspense>
控制,组件自身的加载、错误、延迟和超时选项都将被忽略。
模板引用 ref
<input ref="input" />
const app = Vue.createApp({})
app.component('base-input', {
template: `
<input ref="input" />
`,
methods: {
focusInput() {
this.$refs.input.focus()
}
},
mounted() {
this.focusInput()
}
})
还可以向组件本身添加另一个 ref
,并使用它从父组件触发 focusInput
事件:
<base-input ref="usernameInput"></base-input>
this.$refs.usernameInput.focusInput()
应该避免在模板或计算属性中访问 $refs
。
控制更新$forceUpdate
需要在 Vue 中强制更新,在 99.99%的情况下,你在某个地方犯了错误。可能依赖于 Vue 响应性系统未跟踪的状态,比如在组件创建之后添加了 data
属性。
过渡 & 动画概述
Vue 提供了一些抽象概念,可以帮助处理过渡和动画,特别是在响应某些变化时
- 在 CSS 和 JS 中,使用内置的
<transition>
组件来钩住组件中进入和离开 DOM。 - 过渡模式,以便你在过渡期间编排顺序。
- 在处理多个元素位置更新时,使用
<transition-group>
组件,通过 FLIP 技术来提高性能。 - 使用
watchers
来处理应用中不同状态的过渡。
基于 class 的动画和过渡
<div id="demo">
Push this button to do something you shouldn't be doing:<br />
<div :class="{ shake: noActivated }">
<button @click="noActivated = true">Click me</button>
<span v-if="noActivated">Oh no!</span>
</div>
</div>
const Demo = {
data() {
return {
noActivated: false
}
}
}
Vue.createApp(Demo).mount('#demo')
.shake {
animation: shake 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
transform: translate3d(0, 0, 0);
backface-visibility: hidden;
perspective: 1000px;
}
@keyframes shake {
10%,
90% {
transform: translate3d(-1px, 0, 0);
}
20%,
80% {
transform: translate3d(2px, 0, 0);
}
30%,
50%,
70% {
transform: translate3d(-4px, 0, 0);
}
40%,
60% {
transform: translate3d(4px, 0, 0);
}
}
过渡与 Style 绑定
<div id="demo">
<div
@mousemove="xCoordinate"
:style="{ backgroundColor: `hsl(${x}, 80%, 50%)` }"
class="movearea"
>
<h3>Move your mouse across the screen...</h3>
<p>x: {{x}}</p>
</div>
</div>
const Demo = {
data() {
return {
x: 0
}
},
methods: {
xCoordinate(e) {
this.x = e.clientX
}
}
}
Vue.createApp(Demo).mount('#demo')
.movearea {
transition: 0.2s background-color ease;
}
性能
尽可能对元素动画进行硬件加速,并使用不触发重绘的 property
Transform 和 Opacity
更改 transform 不会触发任何几何形状变化或绘制。这意味着该操作可能是由合成器线程在 GPU 的帮助下执行。
opacity
属性的行为也类似。因此,他们是在 web 上做元素移动的理想选择。
硬件加速
perspective
、backface-visibility
和transform:translateZ(x)
等 property 将让浏览器知道你需要硬件加速。
如果要对一个元素进行硬件加速,可以应用以下任何一个 property (并不是需要全部,任意一个就可以):
perspective: 1000px;
backface-visibility: hidden;
transform: translateZ(0);
运动时长 0.1s 到 0.4s 之间的计时, 0.25s 是一个最佳选择
如果你有一些元素需要移动更大的距离,或者有更多的步骤或状态变化,0.25s 并不会有很好的效果,你将不得不有更多的目的性,而且定时也需要更加独特。
单元素/组件的过渡
在下列情形中,可以给任何元素和组件添加进入/离开过渡
- 条件渲染 (使用
v-if
) - 条件展示 (使用
v-show
) - 动态组件
- 组件根节点
<div id="demo">
<button @click="show = !show">
Toggle
</button>
<transition name="fade">
<p v-if="show">hello</p>
</transition>
</div>
const Demo = {
data() {
return {
show: true
}
}
}
Vue.createApp(Demo).mount('#demo')
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
当插入或删除包含在 transition
组件中的元素时,Vue 将会做以下处理:
- 自动嗅探目标元素是否应用了 CSS 过渡或动画,如果是,在恰当的时机添加/删除 CSS 类名。
- 如果过渡组件提供了 JavaScript 钩子函数 ,这些钩子函数将在恰当的时机被调用。
- 如果没有找到 JavaScript 钩子并且也没有检测到 CSS 过渡/动画,DOM 操作 (插入/删除) 在下一帧中立即执行。(注意:此指浏览器逐帧动画机制,和 Vue 的
nextTick
概念不同)
过渡class
在进入/离开的过渡中,会有 6 个 class 切换。
v-enter-from
:定义进入过渡的开始状态。在元素被插入之前生效,在元素被插入之后的下一帧移除。v-enter-active
:定义进入过渡生效时的状态。在整个进入过渡的阶段中应用,在元素被插入之前生效,在过渡/动画完成之后移除。这个类可以被用来定义进入过渡的过程时间
,延迟
和曲线函数
。v-enter-to
:定义进入过渡的结束状态。在元素被插入之后下一帧生效 (与此同时v-enter-from
被移除),在过渡/动画完成之后移除。v-leave-from
:定义离开过渡的开始状态。在离开过渡被触发时立刻生效,下一帧被移除。v-leave-active
:定义离开过渡生效时的状态。在整个离开过渡的阶段中应用,在离开过渡被触发时立刻生效,在过渡/动画完成之后移除。这个类可以被用来定义离开过渡的过程时间
,延迟
和曲线函数
。v-leave-to
:离开过渡的结束状态。在离开过渡被触发之后下一帧生效 (与此同时v-leave-from
被删除),在过渡/动画完成之后移除。
使用了
<transition name="my-transition">
,那么v-enter-from
会替换为my-transition-enter-from
。
自定义过渡 class 类名
可以通过以下 attribute 来自定义过渡类名:
enter-from-class
enter-active-class
enter-to-class
leave-from-class
leave-active-class
leave-to-class
<link
href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.0/animate.min.css"
rel="stylesheet"
type="text/css"
/>
<div id="demo">
<button @click="show = !show">
Toggle render
</button>
<transition
name="custom-classes-transition"
enter-active-class="animate__animated animate__tada"
leave-active-class="animate__animated animate__bounceOutRight"
>
<p v-if="show">hello</p>
</transition>
</div>
const Demo = {
data() {
return {
show: true
}
}
}
Vue.createApp(Demo).mount('#demo')
过度的JavaScript 钩子
<transition
@before-enter="beforeEnter"
@enter="enter"
@after-enter="afterEnter"
@enter-cancelled="enterCancelled"
@before-leave="beforeLeave"
@leave="leave"
@after-leave="afterLeave"
@leave-cancelled="leaveCancelled"
:css="false"
>
<!-- ... -->
</transition>
// ...
methods: {
// --------
// ENTERING
// --------
beforeEnter(el) {
// ...
},
// 当与 CSS 结合使用时
// 回调函数 done 是可选的
enter(el, done) {
// ...
done()
},
afterEnter(el) {
// ...
},
enterCancelled(el) {
// ...
},
// --------
// 离开时
// --------
beforeLeave(el) {
// ...
},
// 当与 CSS 结合使用时
// 回调函数 done 是可选的
leave(el, done) {
// ...
done()
},
afterLeave(el) {
// ...
},
// leaveCancelled 只用于 v-show 中
leaveCancelled(el) {
// ...
}
}
组合式 API(Composition API)
setup
组件选项
为了开始使用组合式 API,我们首先需要一个可以实际使用它的地方。在 Vue 组件中,我们将此位置称为 setup
。
新的
setup
选项在组件创建之前执行,一旦props
被解析,就将作为组合式 API 的入口。
在 setup
中你应该避免使用 this
,因为它不会找到组件实例
setup
的调用发生在data
、computed
或methods
被解析之前,所以它们无法在setup
中被获取。
setup
选项是一个接收props
和context
的函数,此外,将setup
返回的所有内容都暴露给组件的其余部分 (计算属性、方法、生命周期钩子等等) 以及组件的模板。
export default {
components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
props: {
user: {
type: String,
required: true
}
},
setup(props) {
console.log(props) // { user: '' }
return {} // 这里返回的任何内容都可以用于组件的其余部分
}
// 组件的“其余部分”
}
仓库列表示例:仓库列表的视图有搜索和筛选功能
该组件有以下几个职责:
- 从假定的外部 API 获取该用户的仓库,并在用户有任何更改时进行刷新
- 使用
searchQuery
字符串搜索仓库 - 使用
filters
对象筛选仓库
原本的实现
// src/components/UserRepositories.vue
export default {
components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
props: {
user: {
type: String,
required: true
}
},
data () {
return {
repositories: [], // 1
filters: { ... }, // 3
searchQuery: '' // 2
}
},
computed: {
filteredRepositories () { ... }, // 3
repositoriesMatchingSearchQuery () { ... }, // 2
},
watch: {
user: 'getUserRepositories' // 1
},
methods: {
getUserRepositories () {
// 使用 `this.user` 获取用户仓库
}, // 1
updateFilters () { ... }, // 3
},
mounted () {
this.getUserRepositories() // 1
}
}
1. 从假定的外部 API 获取该用户的仓库,并在用户有任何更改时进行刷新
- 仓库列表
- 更新仓库列表的函数
- 返回列表和函数,以便其他组件选项可以对它们进行访问
// src/components/UserRepositories.vue `setup` function
import { fetchUserRepositories } from '@/api/repositories'
// 在我们的组件内
setup (props) {
let repositories = []
const getUserRepositories = async () => {
repositories = await fetchUserRepositories(props.user)
}
return {
repositories,//还无法生效,因为 `repositories` 变量是非响应式的。这意味着从用户的角度来看,仓库列表将始终为空。
getUserRepositories // 返回的函数与方法的行为相同
}
}
上编代码还无法生效,因为 repositories
变量是非响应式的。这意味着从用户的角度来看,仓库列表将始终为空。
带 ref
的响应式变量
在 Vue 3.0 中,我们可以通过一个新的
ref
函数使任何响应式变量在任何地方起作用,如下所示:
import { ref } from 'vue'
const counter = ref(0)
ref
接收参数并将其包裹在一个带有value
property 的对象中返回,然后可以使用该 property 访问或更改响应式变量的值:
import { ref } from 'vue'
const counter = ref(0)
console.log(counter) // { value: 0 }
console.log(counter.value) // 0
counter.value++
console.log(counter.value) // 1
将值封装在一个对象中,看似没有必要,但为了保持 JavaScript 中不同数据类型的行为统一,这是必须的。
ref
为我们的值创建了一个响应式引用。在整个组合式 API 中会经常使用引用的概念。
到仓库列表示例,让我们创建一个响应式的 repositories
变量
// src/components/UserRepositories.vue `setup` function
import { fetchUserRepositories } from '@/api/repositories'
import { ref } from 'vue'
// 在我们的组件中
setup (props) {
const repositories = ref([])
const getUserRepositories = async () => {
repositories.value = await fetchUserRepositories(props.user)
}
return {
repositories,
getUserRepositories
}
}
现在,每当我们调用 getUserRepositories
时,repositories
都将发生变化,视图也会更新以反映变化
在 setup
内注册生命周期钩子
为了使组合式 API 的功能和选项式 API 一样完整,我们还需要一种在
setup
中注册生命周期钩子的方法。组合式 API 上的生命周期钩子与选项式 API 的名称相同,但前缀为
on
:即mounted
看起来会像onMounted
。这些函数接受一个回调,当钩子被组件调用时,该回调将被执行。
// src/components/UserRepositories.vue `setup` function
import { fetchUserRepositories } from '@/api/repositories'
import { ref, onMounted } from 'vue'
// 在我们的组件中
setup (props) {
const repositories = ref([])
const getUserRepositories = async () => {
repositories.value = await fetchUserRepositories(props.user)
}
onMounted(getUserRepositories) // 在 `mounted` 时调用 `getUserRepositories`
return {
repositories,
getUserRepositories
}
}
watch
响应式更改
在组件中使用 watch
选项并在 user
上设置侦听器一样,我们也可以使用从 Vue 导入的 watch
函数执行相同的操作。它接受 3 个参数:
- 一个想要侦听的响应式引用或 getter 函数
- 一个回调
- 可选的配置选项
import { ref, watch } from 'vue'
const counter = ref(0)
watch(counter, (newValue, oldValue) => {
console.log('The new counter value is: ' + counter.value)
})
以下是等效的选项式 API:
export default {
data() {
return {
counter: 0
}
},
watch: {
counter(newValue, oldValue) {
console.log('The new counter value is: ' + this.counter)
}
}
}
现在我们将其应用到仓库列表示例中:
// src/components/UserRepositories.vue `setup` function
import { fetchUserRepositories } from '@/api/repositories'
import { ref, onMounted, watch, toRefs } from 'vue'
// 在我们组件中
setup (props) {
// 使用 `toRefs` 创建对prop的 `user` property 的响应式引用
const { user } = toRefs(props)
const repositories = ref([])
const getUserRepositories = async () => {
// 更新 `prop.user` 到 `user.value` 访问引用值
repositories.value = await fetchUserRepositories(user.value)
}
onMounted(getUserRepositories)
// 在 user prop 的响应式引用上设置一个侦听器
watch(user, getUserRepositories)
return {
repositories,
getUserRepositories
}
}
在我们的
setup
的顶部使用了toRefs
。这是为了确保我们的侦听器能够根据user
prop 的变化做出反应。
独立的 computed
属性
可以使用从 Vue 导入的 computed
函数在 Vue 组件外部创建计算属性
import { ref, computed } from 'vue'
const counter = ref(0)
const twiceTheCounter = computed(() => counter.value * 2)
counter.value++
console.log(counter.value) // 1
console.log(twiceTheCounter.value) // 2
给
computed
函数传递了第一个参数,它是一个类似 getter 的回调函数,输出的是一个只读的响应式引用。为了访问新创建的计算变量的 value,我们需要像
ref
一样使用.value
2.仓库列表示例:基于 searchQuery
进行过滤,这次是使用计算属性
// src/components/UserRepositories.vue `setup` function
import { fetchUserRepositories } from '@/api/repositories'
import { ref, onMounted, watch, toRefs, computed } from 'vue'
// 在我们的组件中
setup (props) {
// 使用 `toRefs` 创建对 props 中的 `user` property 的响应式引用
const { user } = toRefs(props)
const repositories = ref([])
const getUserRepositories = async () => {
// 更新 `props.user ` 到 `user.value` 访问引用值
repositories.value = await fetchUserRepositories(user.value)
}
onMounted(getUserRepositories)
// 在 user prop 的响应式引用上设置一个侦听器
watch(user, getUserRepositories)
const searchQuery = ref('')
const repositoriesMatchingSearchQuery = computed(() => {
return repositories.value.filter(
repository => repository.name.includes(searchQuery.value)
)
})
return {
repositories,
getUserRepositories,
searchQuery,
repositoriesMatchingSearchQuery
}
}
提取一个独立的组合式函数
创建 useUserRepositories
函数:
// src/composables/useUserRepositories.js
import { fetchUserRepositories } from '@/api/repositories'
import { ref, onMounted, watch } from 'vue'
export default function useUserRepositories(user) {
const repositories = ref([])
const getUserRepositories = async () => {
repositories.value = await fetchUserRepositories(user.value)
}
onMounted(getUserRepositories)
watch(user, getUserRepositories)
return {
repositories,
getUserRepositories
}
}
然后是搜索功能:
// src/composables/useRepositoryNameSearch.js
import { ref, computed } from 'vue'
export default function useRepositoryNameSearch(repositories) {
const searchQuery = ref('')
const repositoriesMatchingSearchQuery = computed(() => {
return repositories.value.filter(repository => {
return repository.name.includes(searchQuery.value)
})
})
return {
searchQuery,
repositoriesMatchingSearchQuery
}
}
现在我们有了两个单独的功能模块,接下来就可以开始在组件中使用它们了。以下是如何做到这一点:
// src/components/UserRepositories.vue
import useUserRepositories from '@/composables/useUserRepositories'
import useRepositoryNameSearch from '@/composables/useRepositoryNameSearch'
import { toRefs } from 'vue'
export default {
components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
props: {
user: {
type: String,
required: true
}
},
setup (props) {
const { user } = toRefs(props)
const { repositories, getUserRepositories } = useUserRepositories(user)
const {
searchQuery,
repositoriesMatchingSearchQuery
} = useRepositoryNameSearch(repositories)
return {
// 因为我们并不关心未经过滤的仓库
// 我们可以在 `repositories` 名称下暴露过滤后的结果
repositories: repositoriesMatchingSearchQuery,
getUserRepositories,
searchQuery,
}
},
data () {
return {
filters: { ... }, // 3
}
},
computed: {
filteredRepositories () { ... }, // 3
},
methods: {
updateFilters () { ... }, // 3
}
}
Setup
参数
props
context
Props
setup
函数中的第一个参数是 props
。正如在一个标准组件中所期望的那样,setup
函数中的 props
是响应式的,当传入新的 prop 时,它将被更新。
因为
props
是响应式的,你不能使用 ES6 解构,它会消除 prop 的响应性
// MyBook.vue
export default {
props: {
title: String
},
setup(props) {
console.log(props.title)
}
}
如果需要解构 prop,可以在
setup
函数中使用toRefs
函数来完成此操作:
// MyBook.vue
import { toRefs } from 'vue'
setup(props) {
const { title } = toRefs(props)
console.log(title.value)
}
如果 title
是可选的 prop,则传入的 props
中可能没有 title
。在这种情况下,toRefs
将不会为 title
创建一个 ref 。你需要使用 toRef
替代它:
// MyBook.vue
import { toRef } from 'vue'
setup(props) {
const title = toRef(props, 'title')
console.log(title.value)
}
Context
传递给 setup
函数的第二个参数是 context
。context
是一个普通的 JavaScript 对象,它暴露组件的三个 property:attrs、slots、emit
// MyBook.vue
export default {
setup(props, context) {
// Attribute (非响应式对象)
console.log(context.attrs)
// 插槽 (非响应式对象)
console.log(context.slots)
// 触发事件 (方法)
console.log(context.emit)
}
}
context
是一个普通的 JavaScript 对象,也就是说,它不是响应式的,这意味着你可以安全地对 context
使用 ES6 解构:
// MyBook.vue
export default {
setup(props, { attrs, slots, emit }) {
...
}
}
attrs
和 slots
是有状态的对象,它们总是会随组件本身的更新而更新。这意味着你应该避免对它们进行解构,并始终以 attrs.x
或 slots.x
的方式引用 property。
访问组件的 property
执行 setup
时,组件实例尚未被创建。因此,你只能访问以下 property:
props
attrs
slots
emit
换句话说,你将无法访问以下组件选项:
data
computed
methods
结合模板使用
如果 setup
返回一个对象,那么该对象的 property 以及传递给 setup
的 props
参数中的 property 就都可以在模板中访问到:
<!-- MyBook.vue -->
<template>
<div>{{ collectionName }}: {{ readersNumber }} {{ book.title }}</div>
</template>
<script>
import { ref, reactive } from 'vue'
export default {
props: {
collectionName: String
},
setup(props) {
const readersNumber = ref(0)
const book = reactive({ title: 'Vue 3 Guide' })
// 暴露给 template
return {
readersNumber,
book
}
}
}
</script>
使用渲染函数
setup
还可以返回一个渲染函数,该函数可以直接使用在同一作用域中声明的响应式状态:
// MyBook.vue
import { h, ref, reactive } from 'vue'
export default {
setup() {
const readersNumber = ref(0)
const book = reactive({ title: 'Vue 3 Guide' })
// 请注意这里我们需要显式调用 ref 的 value
return () => h('div', [readersNumber.value, book.title])
}
}
使用 this
在 setup()
内部,this
不是该活跃实例的引用,因为 setup()
是在解析其它组件选项之前被调用的,所以 setup()
内部的 this
的行为与其它选项中的 this
完全不同。这使得 setup()
在和其它选项式 API 一起使用时可能会导致混淆。
生命周期钩子
下表包含如何在 setup () 内部调用生命周期钩子:
选项式 API | Hook inside setup |
---|---|
beforeCreate | Not needed* |
created | Not needed* |
beforeMount | onBeforeMount |
mounted | onMounted |
beforeUpdate | onBeforeUpdate |
updated | onUpdated |
beforeUnmount | onBeforeUnmount |
unmounted | onUnmounted |
errorCaptured | onErrorCaptured |
renderTracked | onRenderTracked |
renderTriggered | onRenderTriggered |
activated | onActivated |
deactivated | onDeactivated |
setup
是围绕 beforeCreate
和 created
生命周期钩子运行的,所以不需要显式地定义它们。换句话说,在这些钩子中编写的任何代码都应该直接在 setup
函数中编写
这些函数接受一个回调函数,当钩子被组件调用时将会被执行:
// MyBook.vue
export default {
setup() {
// mounted
onMounted(() => {
console.log('Component is mounted!')
})
}
}
Provide / Inject
在当前活动实例的 setup()
期间调用
<!-- src/components/MyMap.vue -->
<template>
<MyMarker />
</template>
<script>
import MyMarker from './MyMarker.vue'
export default {
components: {
MyMarker
},
provide: {
location: 'North Pole',
geolocation: {
longitude: 90,
latitude: 135
}
}
}
</script>
<!-- src/components/MyMarker.vue -->
<script>
export default {
inject: ['location', 'geolocation']
}
</script>
在 setup()
中使用 provide
首先从 vue
显式导入 provide
方法。这使我们能够调用 provide
来定义每个 property。
provide
函数允许你通过两个参数定义 property:
- name (
<String>
类型) - value
使用 MyMap
组件后,provide 的值可以按如下方式重构:
<!-- src/components/MyMap.vue -->
<template>
<MyMarker />
</template>
<script>
import { provide } from 'vue'
import MyMarker from './MyMarker.vue'
export default {
components: {
MyMarker
},
setup() {
provide('location', 'North Pole')
provide('geolocation', {
longitude: 90,
latitude: 135
})
}
}
</script>
添加响应性
为了增加 provide 值和 inject 值之间的响应性,我们可以在 provide 值时使用 ref 或 reactive
<!-- src/components/MyMap.vue -->
<template>
<MyMarker />
</template>
<script>
import { provide, reactive, ref } from 'vue'
import MyMarker from './MyMarker.vue'
export default {
components: {
MyMarker
},
setup() {
const location = ref('North Pole')
const geolocation = reactive({
longitude: 90,
latitude: 135
})
provide('location', location)
provide('geolocation', geolocation)
}
}
</script>
修改响应式 property
当使用响应式 provide / inject 值时,建议尽可能将对响应式 property 的所有修改限制在定义 provide 的组件内部。
<!-- src/components/MyMap.vue -->
<template>
<MyMarker />
</template>
<script>
import { provide, reactive, ref } from 'vue'
import MyMarker from './MyMarker.vue'
export default {
components: {
MyMarker
},
setup() {
const location = ref('North Pole')
const geolocation = reactive({
longitude: 90,
latitude: 135
})
provide('location', location)
provide('geolocation', geolocation)
return {
location
}
},
methods: {
updateLocation() {
this.location = 'South Pole'
}
}
}
</script>
有时我们需要在注入数据的组件内部更新 inject 的数据。在这种情况下,我们建议 provide 一个方法来负责改变响应式 property。
<!-- src/components/MyMap.vue -->
<template>
<MyMarker />
</template>
<script>
import { provide, reactive, ref } from 'vue'
import MyMarker from './MyMarker.vue'
export default {
components: {
MyMarker
},
setup() {
const location = ref('North Pole')
const geolocation = reactive({
longitude: 90,
latitude: 135
})
const updateLocation = () => {
location.value = 'South Pole'
}
provide('location', location)
provide('geolocation', geolocation)
provide('updateLocation', updateLocation)
}
}
</script>
<!-- src/components/MyMarker.vue -->
<script>
import { inject } from 'vue'
export default {
setup() {
const userLocation = inject('location', 'The Universe')
const userGeolocation = inject('geolocation')
const updateUserLocation = inject('updateLocation')
return {
userLocation,
userGeolocation,
updateUserLocation
}
}
}
</script>
确保通过 provide
传递的数据不会被 inject 的组件更改,我们建议对提供者的 property 使用 readonly
<!-- src/components/MyMap.vue -->
<template>
<MyMarker />
</template>
<script>
import { provide, reactive, readonly, ref } from 'vue'
import MyMarker from './MyMarker.vue'
export default {
components: {
MyMarker
},
setup() {
const location = ref('North Pole')
const geolocation = reactive({
longitude: 90,
latitude: 135
})
const updateLocation = () => {
location.value = 'South Pole'
}
provide('location', readonly(location))
provide('geolocation', readonly(geolocation))
provide('updateLocation', updateLocation)
}
}
</script>
模板引用(ref)
在使用组合式 API 时,响应式引用和模板引用的概念是统一的。为了获得对模板内元素或组件实例的引用,我们可以像往常一样声明 ref 并从 setup() 返回:
<template>
<div ref="root">This is a root element</div>
</template>
<script>
import { ref, onMounted } from 'vue'
export default {
setup() {
const root = ref(null)
onMounted(() => {
// DOM 元素将在初始渲染后分配给 ref
console.log(root.value) // <div>This is a root element</div>
})
return {
root
}
}
}
</script>
在上边的步骤中:
1、在渲染上下文中暴露 root
2、通过 ref="root"
,将其绑定到 div 作为其 ref
在虚拟 DOM 补丁算法中,如果 VNode 的
ref
键对应于渲染上下文中的 ref,则 VNode 的相应元素或组件实例将被分配给该 ref 的值。
JSX 中的用法
export default {
setup() {
const root = ref(null)
return () =>
h('div', {
ref: root
})
// with JSX
return () => <div ref={root} />
}
}
v-for
中的用法(页面中有多个ref)
组合式 API 模板引用在
v-for
内部使用时没有特殊处理。相反,请使用函数引用执行自定义处理:
<template>
<div v-for="(item, i) in list" :ref="el => { if (el) divs[i] = el }">
{{ item }}
</div>
</template>
<script>
import { ref, reactive, onBeforeUpdate } from 'vue'
export default {
setup() {
const list = reactive([1, 2, 3])
const divs = ref([])
// 确保在每次更新之前重置ref
onBeforeUpdate(() => {
divs.value = []
})
return {
list,
divs
}
}
}
</script>
侦听模板引用
侦听模板引用的变更可以替代前面例子中演示使用的生命周期钩子。
但与生命周期钩子的一个关键区别是,
watch()
和watchEffect()
在 DOM 挂载或更新之前运行副作用,所以当侦听器运行时,模板引用还未被更新。
<template>
<div ref="root">This is a root element</div>
</template>
<script>
import { ref, watchEffect } from 'vue'
export default {
setup() {
const root = ref(null)
watchEffect(() => {
// 这个副作用在 DOM 更新之前运行,因此,模板引用还没有持有对元素的引用。
console.log(root.value) // => null
})
return {
root
}
}
}
</script>
使用模板引用的侦听器应该用
flush: 'post'
选项来定义,这将在 DOM 更新后运行副作用,确保模板引用与 DOM 保持同步,并引用正确的元素。
<template>
<div ref="root">This is a root element</div>
</template>
<script>
import { ref, watchEffect } from 'vue'
export default {
setup() {
const root = ref(null)
watchEffect(() => {
console.log(root.value) // => <div>This is a root element</div>
},
{
flush: 'post'
})
return {
root
}
}
}
</script>
Mixin
Mixin 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。
一个 mixin 对象可以包含任意组件选项。
当组件使用 mixin 对象时,所有 mixin 对象的选项将被“混合”进入该组件本身的选项。
// define a mixin object
const myMixin = {
created() {
this.hello()
},
methods: {
hello() {
console.log('hello from mixin!')
}
}
}
// define an app that uses this mixin
const app = Vue.createApp({
mixins: [myMixin]
})
app.mount('#mixins-basic') // => "hello from mixin!"
data选项合并
每个 mixin 可以拥有自己的 data
函数。每个 data
函数都会被调用,并将返回结果合并。在数据的 property 发生冲突时,会以组件自身的数据为优先。
const myMixin = {
data() {
return {
message: 'hello',
foo: 'abc'
}
}
}
const app = Vue.createApp({
mixins: [myMixin],
data() {
return {
message: 'goodbye',
bar: 'def'
}
},
created() {
console.log(this.$data) // => { message: "goodbye", foo: "abc", bar: "def" }
}
})
钩子函数合并
同名钩子函数将合并为一个数组,因此都将被调用。
mixin 对象的钩子将在组件自身钩子之前调用
const myMixin = {
created() {
console.log('mixin 对象的钩子被调用')
}
}
const app = Vue.createApp({
mixins: [myMixin],
created() {
console.log('组件钩子被调用')
}
})
// => "mixin 对象的钩子被调用"
// => "组件钩子被调用"
值为对象的选项 methods
、components
和 directives
,将被合并为同一个对象。两个对象键名冲突时,取组件对象的键值对
const myMixin = {
methods: {
foo() {
console.log('foo')
},
conflicting() {
console.log('from mixin')
}
}
}
const app = Vue.createApp({
mixins: [myMixin],
methods: {
bar() {
console.log('bar')
},
conflicting() {
console.log('from self')
}
}
})
const vm = app.mount('#mixins-basic')
vm.foo() // => "foo"
vm.bar() // => "bar"
vm.conflicting() // => "from self"
全局 mixin
const app = Vue.createApp({
myOption: 'hello!'
})
// 为自定义的选项 'myOption' 注入一个处理器。
app.mixin({
created() {
const myOption = this.$options.myOption
if (myOption) {
console.log(myOption)
}
}
})
app.mount('#mixins-global') // => "hello!"
Mixin 也可以进行全局注册。使用时格外小心!一旦使用全局 mixin,它将影响每一个之后创建的组件 (例如,每个子组件)。
const app = Vue.createApp({
myOption: 'hello!'
})
// 为自定义的选项 'myOption' 注入一个处理器。
app.mixin({
created() {
const myOption = this.$options.myOption
if (myOption) {
console.log(myOption)
}
}
})
// 将myOption也添加到子组件
app.component('test-component', {
myOption: 'hello from component!'
})
app.mount('#mixins-global')
// => "hello!"
// => "hello from component!"
自定义选项合并策略app.config.optionMergeStrategies
合并策略接收在父实例和子实例上定义的该选项的值,分别作为第一个和第二个参数
const app = Vue.createApp({})
app.config.optionMergeStrategies.customOption = (toVal, fromVal) => {
// return mergedVal
}
const app = Vue.createApp({
custom: 'hello!'
})
app.config.optionMergeStrategies.custom = (toVal, fromVal) => {
console.log(fromVal, toVal)
// => "goodbye!", undefined
// => "hello", "goodbye!"
return fromVal || toVal
}
app.mixin({
custom: 'goodbye!',
created() {
console.log(this.$options.custom) // => "hello!"
}
})
自定义指令
需要对普通 DOM 元素进行底层操作,这时候就会用到自定义指令
全局注册
const app = Vue.createApp({})
// 注册一个全局自定义指令 `v-focus`
app.directive('focus', {
// 当被绑定的元素挂载到 DOM 中时……
mounted(el) {
// 聚焦元素
el.focus()
}
})
局部注册
组件中也接受一个 directives
的选项:
directives: {
focus: {
// 指令的定义
mounted(el) {
el.focus()
}
}
}
使用
<input v-focus />
钩子函数
一个指令定义对象可以提供如下几个钩子函数 (均为可选):
created
:在绑定元素的 attribute 或事件监听器被应用之前调用。在指令需要附加须要在普通的v-on
事件监听器前调用的事件监听器时,这很有用。beforeMount
:当指令第一次绑定到元素并且在挂载父组件之前调用。mounted
:在绑定元素的父组件被挂载后调用。beforeUpdate
:在更新包含组件的 VNode 之前调用。updated
:在包含组件的 VNode 及其子组件的 VNode 更新后调用。beforeUnmount
:在卸载绑定元素的父组件之前调用unmounted
:当指令与元素解除绑定且父组件已卸载时,只调用一次。
动态指令参数
自定义指令 API 钩子函数的参数 (即 el
、binding
、vnode
和 prevVnode
)
指令的参数可以是动态的。例如,在
v-mydirective:[argument]="value"
中,argument
参数可以根据组件实例数据进行更新!这使得自定义指令可以在应用中被灵活使用
示例:
<div id="dynamicexample">
<h2>Scroll down the page</h2>
<input type="range" min="0" max="500" v-model="pinPadding">
<p v-pin:[direction]="pinPadding">Stick me {{ pinPadding + 'px' }} from the {{ direction || 'top' }} of the page</p>
</div>
const app = Vue.createApp({
data() {
return {
direction: 'right',
pinPadding: 200
}
}
})
app.directive('pin', {
mounted(el, binding) {
el.style.position = 'fixed'
const s = binding.arg || 'top'
el.style[s] = binding.value + 'px'
},
updated(el, binding) {
const s = binding.arg || 'top'
el.style[s] = binding.value + 'px'
}
})
app.mount('#dynamic-arguments-example')
函数简写
在 mounted
和 updated
时触发相同行为,而不关心其他的钩子函数。那么你可以通过将这个回调函数传递给指令来实现:
app.directive('pin', (el, binding) => {
el.style.position = 'fixed'
const s = binding.arg || 'top'
el.style[s] = binding.value + 'px'
})
对象字面量
如果指令需要多个值,可以传入一个 JavaScript 对象字面量。记住,指令函数能够接受所有合法的 JavaScript 表达式。
<div v-demo="{ color: 'white', text: 'hello!' }"></div>
app.directive('demo', (el, binding) => {
console.log(binding.value.color) // => "white"
console.log(binding.value.text) // => "hello!"
})
在组件中使用
和非 prop 的 attribute 类似,当在组件中使用时,自定义指令总是会被应用在组件的根节点上
<my-component v-demo="test"></my-component>
app.component('my-component', {
template: `
<div> // v-demo 指令将会被应用在这里
<span>My component content</span>
</div>
`
})
和 attribute 不同,指令不会通过 v-bind="$attrs"
被传入另一个元素
Teleport
有时组件模板的一部分
逻辑上属于该组件
,而从技术角度
来看,最好将模板的这一部分移动到 DOM 中 Vue app 之外
的其他位置。
Teleport
提供了一种干净的方法,允许我们控制在 DOM 中哪个父节点下渲染了 HTML
,而不必求助于全局状态或将其拆分为两个组件。
<teleport to="标签名"></teleport>
常见的场景
是创建一个包含全屏模式的模态框组件
希望模态框的逻辑存在于组件中,但是模态框的快速定位就很难通过 CSS 来解决,或者需要更改组件组合。
const app = Vue.createApp({});
app.component('modal-button', {
template: `
<button @click="modalOpen = true">
Open full screen modal!
</button>
<div v-if="modalOpen" class="modal">
<div>
I'm a modal!
<button @click="modalOpen = false">
Close
</button>
</div>
</div>
`,
data() {
return {
modalOpen: false
}
}
})
修改
modal-button
以使用<teleport>
,并告诉 Vue “Teleport 这个 HTML 到该‘body’标签”
app.component('modal-button', {
template: `
<button @click="modalOpen = true">
Open full screen modal! (With teleport!)
</button>
<teleport to="body">
<div v-if="modalOpen" class="modal">
<div>
I'm a teleported modal!
(My parent is "body")
<button @click="modalOpen = false">
Close
</button>
</div>
</div>
</teleport>
`,
data() {
return {
modalOpen: false
}
}
})
与 Vue components 一起使用
如果
<teleport>
包含 Vue 组件,则它仍将是<teleport>
父组件的逻辑子组件:来自父组件的注入按预期工作,并且子组件将嵌套在 Vue Devtools 中的父组件之下,而不是放在实际内容移动到的位置
const app = Vue.createApp({
template: `
<h1>Root instance</h1>
<parent-component />
`
})
app.component('parent-component', {
template: `
<h2>This is a parent component</h2>
<teleport to="#endofbody">
<child-component name="John" />
</teleport>
`
})
app.component('child-component', {
props: ['name'],
template: `
<div>Hello, {{ name }}</div>
`
})
在这种情况下,即使在不同的地方渲染
child-component
,它仍将是parent-component
的子级,并将从中接收name
prop
在同一目标上使用多个 teleport
一个可重用的
<Modal>
组件,它可能同时有多个实例处于活动状态。对于这种情况,多个<teleport>
组件可以将其内容挂载到同一个目标元素。顺序就是挂载的先后顺序
<teleport to="#modals">
<div>A</div>
</teleport>
<teleport to="#modals">
<div>B</div>
</teleport>
<!-- result-->
<div id="modals">
<div>A</div>
<div>B</div>
</div>