PS:自己记录用 看过官网的可忽略哦
渲染函数
Vue 推荐使用模板来创建HTML。
在需要 JavaScript 的完全编程的能力时,可以用渲染函数,它比模板更接近编译器
h()
参数
h()
函数是一个用于创建 vnode 的实用程序。全名
createVNode()
三个参数:标签名&组件名、属性(attribute、prop 和事件)、子节点(包含slot)
如果没有
prop
,那么通常可以将children
作为第二个参数传入。如果会产生歧义,可以将null
作为第二个参数传入,将children
作为第三个参数传入
// @returns {VNode}
h(
// {String | Object | Function} tag
// 一个 HTML 标签名、一个组件、一个异步组件、或
// 一个函数式组件。
//
// 必需的。
'div',
// {Object} props
// 与 attribute、prop 和事件相对应的对象。
// 我们会在模板中使用。
//
// 可选的。
{},
// {String | Array | Object} children
// 子 VNodes, 使用 `h()` 构建,
// 或使用字符串获取 "文本 Vnode" 或者
// 有插槽的对象。
//
// 可选的。
[
'Some text comes first.',
h('h1', 'A headline'),
h(MyComponent, {
someProp: 'foobar'
})
]
)
示例:通过 level
prop 动态生成标题 (heading) 的组件
const { createApp } = Vue
const app = createApp({})
app.component('anchored-heading', {
template: `
<h1 v-if="level === 1">
<slot></slot>
</h1>
<h2 v-else-if="level === 2">
<slot></slot>
</h2>
<h3 v-else-if="level === 3">
<slot></slot>
</h3>
<h4 v-else-if="level === 4">
<slot></slot>
</h4>
<h5 v-else-if="level === 5">
<slot></slot>
</h5>
<h6 v-else-if="level === 6">
<slot></slot>
</h6>
`,
props: {
level: {
type: Number,
required: true
}
}
})
使用 render
函数重写上面的例子
const { createApp, h } = Vue
const app = createApp({})
app.component('anchored-heading', {
render() {
return h(
'h' + this.level, // tag name
{}, // props/attributes
this.$slots.default() // array of children
)
},
props: {
level: {
type: Number,
required: true
}
}
})
完整实现
const { createApp, h } = Vue
const app = createApp({})
/** 递归地从子节点获取文本 */
function getChildrenTextContent(children) {
return children
.map(node => {
return typeof node.children === 'string'
? node.children
: Array.isArray(node.children)
? getChildrenTextContent(node.children)
: ''
})
.join('')
}
app.component('anchored-heading', {
render() {
// 从 children 的文本内容中创建短横线分隔 (kebab-case) id。
const headingId = getChildrenTextContent(this.$slots.default())
.toLowerCase()
.replace(/\W+/g, '-') // 用短横线替换非单词字符
.replace(/(^-|-$)/g, '') // 删除前后短横线
return h('h' + this.level, [
h(
'a',
{
name: headingId,
href: '#' + headingId
},
this.$slots.default()
)
])
},
props: {
level: {
type: Number,
required: true
}
}
})
注意事项
VNodes 必须唯一
下面的渲染函数是不合法的:
render() {
const myParagraphVNode = h('p', 'hi')
return h('div', [
// 错误 - 重复的 Vnode!
myParagraphVNode, myParagraphVNode
])
}
重复很多次的元素/组件,可以使用工厂函数来实现
render() {
return h('div',
Array.from({ length: 20 }).map(() => {
return h('p', 'hi')
})
)
}
创建组件 VNode
要为某个组件创建一个 VNode,传递给
h
的第一个参数应该是组件本身
render() {
return h(ButtonCounter)
}
如果我们需要通过名称来解析一个组件,那么我们可以调用 resolveComponent
:
resolveComponent
是模板内部用来解析组件名称的同一个函数
render() {
const ButtonCounter = resolveComponent('ButtonCounter')
return h(ButtonCounter)
}
// 我们可以简化为
components: {
ButtonCounter
},
render() {
return h(resolveComponent('ButtonCounter'))
}
可以直接使用它,而不是通过名称注册一个组件,然后再查找:
render() {
return h(ButtonCounter)
}
使用 JavaScript 代替模板功能
v-if
和 v-for
可以在渲染函数中用 JavaScript 的 if
/else
和 map()
来重写:
props: ['items'],
render() {
if (this.items.length) {
return h('ul', this.items.map((item) => {
return h('li', item.name)
}))
} else {
return h('p', 'No items found.')
}
}
v-model
v-model
指令扩展为 modelValue
和 onUpdate:modelValue
props: ['modelValue'],
emits: ['update:modelValue'],
render() {
return h(SomeComponent, {
modelValue: this.modelValue,
'onUpdate:modelValue': value => this.$emit('update:modelValue', value)
})
}
v-on
为事件处理程序提供一个正确的 prop 名称,例如,要处理 click
事件,prop 名称应该是 onClick
render() {
return h('div', {
onClick: $event => console.log('clicked', $event.target)
})
}
事件修饰符
对于
.passive
、.capture
和.once
事件修饰符,可以使用驼峰写法将他们拼接在事件名后面:
render() {
return h('input', {
onClickCapture: this.doThisInCapturingMode,
onKeyupOnce: this.doThisOnce,
onMouseoverOnceCapture: this.doThisOnceInCapturingMode
})
}
对于所有其它的修饰符,私有前缀都不是必须的,因为你可以在事件处理函数中使用事件方法:
修饰符 | 处理函数中的等价操作 |
---|---|
.stop | event.stopPropagation() |
.prevent | event.preventDefault() |
.self | if (event.target !== event.currentTarget) return |
按键: .enter , .13 | if (event.keyCode !== 13) return (对于别的按键修饰符来说,可将 13 改为另一个按键码 |
修饰键: .ctrl , .alt , .shift , .meta | if (!event.ctrlKey) return (将 ctrlKey 分别修改为 altKey , shiftKey , 或 metaKey ) |
render() {
return h('input', {
onKeyUp: event => {
// 如果触发事件的元素不是事件绑定的元素
// 则返回
if (event.target !== event.currentTarget) return
// 如果向上键不是回车键,则终止
// 没有同时按下按键 (13) 和 shift 键
if (!event.shiftKey || event.keyCode !== 13) return
// 停止事件传播
event.stopPropagation()
// 阻止该元素默认的 keyup 事件
event.preventDefault()
// ...
}
})
}
插槽
通过 this.$slots
访问静态插槽的内容,每个插槽都是一个 VNode 数组:
render() {
// `<div><slot></slot></div>`
return h('div', {}, this.$slots.default())
}
props: ['message'],
render() {
// `<div><slot :text="message"></slot></div>`
return h('div', {}, this.$slots.default({
text: this.message
}))
}
要使用渲染函数将插槽传递给子组件,请执行以下操作:
将 slots
以 { name: props => VNode | Array<VNode> }
的形式传递给子对象
const { h, resolveComponent } = Vue
render() {
// `<div><child v-slot="props"><span>{{ props.text }}</span></child></div>`
return h('div', [
h(
resolveComponent('child'),
{},
// 将 `slots` 以 { name: props => VNode | Array<VNode> } 的形式传递给子对象。
{
default: (props) => Vue.h('span', props.text)
}
)
])
}
任何响应式数据都应该在插槽函数内访问,以确保它被注册为子组件的依赖关系,而不是父组件。
相反,对
resolveComponent
的调用应该在插槽函数之外进行,否则它们会相对于错误的组件进行解析。
// `<MyButton><MyIcon :name="icon" />{{ text }}</MyButton>`
render() {
// 应该是在插槽函数外面调用 resolveComponent。
const Button = resolveComponent('MyButton')
const Icon = resolveComponent('MyIcon')
return h(
Button,
null,
{
// 使用箭头函数保存 `this` 的值
default: (props) => {
// 响应式 property 应该在插槽函数内部读取,
// 这样它们就会成为 children 渲染的依赖。
return [
h(Icon, { name: this.icon }),
this.text
]
}
}
)
}
如果一个组件从它的父组件中接收到插槽,它们可以直接传递给子组件
render() {
return h(Panel, null, this.$slots)
}
render() {
return h(
Panel,
null,
{
// 如果我们想传递一个槽函数,我们可以通过
header: this.$slots.header,
// 如果我们需要以某种方式对插槽进行操作,
// 那么我们需要用一个新的函数来包裹它
default: (props) => {
const children = this.$slots.default ? this.$slots.default(props) : []
return children.concat(h('div', 'Extra child'))
}
}
)
}
<component>
和 is
在底层实现里,模板使用 resolveDynamicComponent
来实现 is
attribute。如果我们在 render
函数中需要 is
提供的所有灵活性,我们可以使用同样的函数:
const { h, resolveDynamicComponent } = Vue
// ...
// `<component :is="name"></component>`
render() {
const Component = resolveDynamicComponent(this.name)
return h(Component)
}
is
,resolveDynamicComponent
支持传递一个组件名称、一个 HTML 元素名称或一个组件选项对象如果我们只需要支持组件名称,那么可以使用
resolveComponent
来代替。如果 VNode 始终是一个 HTML 元素,那么我们可以直接把它的名字传递给
h
// `<component :is="bold ? 'strong' : 'em'"></component>`
render() {
return h(this.bold ? 'strong' : 'em')
}
与
<template>
标签一样,<component>
标签仅在模板中作为语法占位符需要,当迁移到render
函数时,应被丢弃
自定义指令:withDirectives
可以使用
withDirectives
将自定义指令应用于 VNode
resolveDirective
是模板内部用来解析指令名称的同一个函数。只有当你还没有直接访问指令的定义对象时,才需要这样做
const { h, resolveDirective, withDirectives } = Vue
// ...
// <div v-pin:top.animate="200"></div>
render () {
const pin = resolveDirective('pin')
return withDirectives(h('div'), [
[pin, 200, 'top', { animate: true }]
])
}
内置组件
如
<keep-alive>
、<transition>
、<transition-group>
和<teleport>
等内置组件默认并没有被全局注册,这也意味着我们无法通过resolveComponent
或resolveDynamicComponent
访问它们在模板中这些组件会被特殊处理,即在它们被用到的时候自动导入。
当我们编写自己的
render
函数时,需要自行导入它们:
const { h, KeepAlive, Teleport, Transition, TransitionGroup } = Vue
// ...
render () {
return h(Transition, { mode: 'out-in' }, /* ... */)
}
渲染函数的返回值
render
函数返回的是单个根 VNode。但其实也有别的选项
返回一个字符串时会创建一个文本 VNode,而不被包裹任何元素
render() {
return 'Hello world!'
}
也可以返回一个子元素数组,而不把它们包裹在一个根结点里。这会创建一个片段 (fragment)
// 相当于模板 `Hello<br>world!`
render() {
return [
'Hello',
h('br'),
'world!'
]
}
组件不需要渲染,这时可以返回 null
。这样在 DOM 中会渲染一个注释节点
JSX
如果需要写很多渲染函数,会很痛苦,有一个 Babel 插件 github.com/vuejs/jsx-n… ,用于在 Vue 中使用 JSX 语法,它可以让我们回到更接近于模板的语法上。 参考:github.com/vuejs/jsx-n…
// 实现<anchored-heading :level="1"> <span>Hello</span> world! </anchored-heading>
import AnchoredHeading from './AnchoredHeading.vue'
const app = createApp({
render() {
return (
<AnchoredHeading level={1}>
<span>Hello</span> world!
</AnchoredHeading>
)
}
})
app.mount('#demo')
函数式组件
函数式组件是自身没有任何状态的组件的另一种形式,在渲染过程中不会创建组件实例,并跳过常规的组件生命周期。
使用的是一个简单函数,而不是一个选项对象,来创建函数式组件
该函数实际上就是该组件的
render
函数。而因为函数式组件里没有this
引用,Vue 会把props
当作第一个参数传入第二个参数
context
包含三个 property:attrs
、emit
和slots
函数式组件可以像普通组件一样被注册和消费。如果你将一个函数作为第一个参数传入
h
,它将会被当作一个函数式组件来对待
const FunctionalComponent = (props, context) => {
// ...
}
可以自定义props
和 emits
FunctionalComponent.props = ['value']
FunctionalComponent.emits = ['click']
插件
插件是自包含的代码,通常向 Vue 添加全局级功能。它可以是公开
install()
方法的object
,也可以是function
插件的功能范围没有严格的限制——一般有下面几种:
- 添加全局方法或者 property。如:vue-custom-element
- 添加全局资源:指令/过滤器/过渡等。如:vue-touch)
- 通过全局 mixin 来添加一些组件选项。(如vue-router)
- 添加全局实例方法,通过把它们添加到
config.globalProperties
上实现。 - 一个库,提供自己的 API,同时提供上面提到的一个或多个功能。如 vue-router
编写插件
插件被添加到应用程序中时,如果它是一个对象,就会调用
install
方法。如果它是一个
function
,则函数本身将被调用。在这两种情况下——它都会收到两个参数:由 Vue 的createApp
生成的app
对象和用户传入的选项
// plugins/i18n.js
export default {
install: (app, options) => {
app.config.globalProperties.$translate = (key) => {
return key.split('.')
.reduce((o, i) => { if (o) return o[i] }, options)
}
app.provide('i18n', options)
app.directive('my-directive', {
mounted (el, binding, vnode, oldVnode) {
// some logic ...
}
...
})
app.mixin({
created() {
// some logic ...
}
...
})
}
}
使用插件
在使用
createApp()
初始化 Vue 应用程序后,你可以通过调用use()
方法将插件添加到你的应用程序中。第一个参数是要安装的插件
第二个参数是可选的,并且取决于每个特定的插件
import { createApp } from 'vue'
import Root from './App.vue'
import i18nPlugin from './plugins/i18n'
const app = createApp(Root)
const i18nStrings = {
greetings: {
hi: 'Hallo!'
}
}
app.use(i18nPlugin, i18nStrings)
app.mount('#app')
深入响应性原理
Vue 最独特的特性之一,是其
非侵入性的响应性系统
。数据模型是被代理的 JavaScript 对象。而当你修改它们时,视图会进行更新。这让状态管理非常简单直观
响应性是一种允许我们以声明式的方式去适应变化的编程范例。
JavaScript 如何实现
- 当一个值被读取时进行追踪,例如
val1 + val2
会同时读取val1
和val2
。 - 当某个值改变时进行检测,例如,当我们赋值
val1 = 3
。 - 重新运行代码来读取原始值,例如,再次运行
sum = val1 + val2
来更新sum
的值。
响应性现在可以在一个独立包中使用。将 $data
包裹在一个代理中的函数被称为 reactive
。我们可以自己直接调用这个函数,允许我们在不需要使用组件的情况下将一个对象包裹在一个响应式代理中
const proxy = reactive({
val1: 2,
val2: 3
})
响应性基础
声明响应式状态reactive(针对引用数据类型)
为 JavaScript 对象创建响应式状态,可以使用 reactive
方法
reactive
相当于 Vue 2.x 中的Vue.observable()
API,为避免与 RxJS 中的 observables 混淆因此对其重命名该 API 返回一个响应式的对象状态
该响应式转换是“深度转换”——它会影响传递对象的所有嵌套 property
import { reactive } from 'vue'
// 响应式状态
const state = reactive({
count: 0
})
创建独立的响应式值作为 refs
(针对基础数据类型)
有一个独立的原始值 (例如,一个字符串),让它变成响应式的:
- 可以创建一个拥有相同字符串 property 的对象,并将其传递给
reactive
。 2.ref
import { ref } from 'vue'
const count = ref(0)
ref
会返回一个可变的响应式对象,该对象只包含一个名为 value
的 property:
import { ref } from 'vue'
const count = ref(0)
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
响应式状态解构
使用大型响应式对象的一些 property 时,可能很想使用 ES6 解构来获取我们想要的 property:
import { reactive } from 'vue'
const book = reactive({
author: 'Vue Team',
year: '2020',
title: 'Vue 3 Guide',
description: 'You are reading this book right now ;)',
price: 'free'
})
let { author, title } = book
使用解构的两个 property 的响应性都会丢失。对于这种情况,我们需要将我们的响应式对象转换为一组 ref。这些 ref 将保留与源对象的响应式关联:
import { reactive, toRefs } from 'vue'
const book = reactive({
author: 'Vue Team',
year: '2020',
title: 'Vue 3 Guide',
description: 'You are reading this book right now ;)',
price: 'free'
})
let { author, title } = toRefs(book)
title.value = 'Vue 3 Detailed Guide' // 我们需要使用 .value 作为标题,现在是 ref
console.log(book.title) // 'Vue 3 Detailed Guide'
响应式计算和侦听
computed
const count = ref(1)
const plusOne = computed({
get: () => count.value + 1,
set: val => {
count.value = val - 1
}
})
plusOne.value = 1
console.log(count.value) // 0
watchEffect
立即执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数
const count = ref(0)
watchEffect(() => console.log(count.value))
// -> logs 0
setTimeout(() => {
count.value++
// -> logs 1
}, 100)
停止侦听
当 watchEffect
在组件的 setup() 函数或生命周期钩子被调用时,侦听器会被链接到该组件的生命周期,并在组件卸载时自动停止。
在一些情况下,也可以显式调用返回值以停止侦听:
const stop = watchEffect(() => {
/* ... */
})
// later
stop()
清除副作用
侦听副作用传入的函数可以接收一个 onInvalidate
函数作入参,用来注册清理失效时的回调
以下情况发生时,这个失效回调会被触发:
- 副作用即将重新执行时
- 侦听器被停止 (如果在
setup()
或生命周期钩子函数中使用了watchEffect
,则在组件卸载时)
watchEffect(onInvalidate => {
const token = performAsyncOperation(id.value)
onInvalidate(() => {
// id has changed or watcher is stopped.
// invalidate previously pending async operation
token.cancel()
})
})
通过传入一个函数去注册失效回调,而不是从回调返回它,是因为返回值对于异步错误处理很重要
在执行数据请求时,副作用函数往往是一个异步函数:
异步函数都会隐式地返回一个 Promise,但是清理函数必须要在 Promise 被 resolve 之前被注册。另外,Vue 依赖这个返回的 Promise 来自动处理 Promise 链上的潜在错误
const data = ref(null)
watchEffect(async onInvalidate => {
onInvalidate(() => {
/* ... */
}) // 我们在Promise解析之前注册清除函数
data.value = await fetchData(props.id)
})
侦听器调试
onTrack
和 onTrigger
选项可用于调试侦听器的行为。
onTrack
将在响应式 property 或 ref 作为依赖项被追踪时被调用。onTrigger
将在依赖项变更导致副作用被触发时被调用。
watchEffect(
() => {
/* 副作用 */
},
{
onTrigger(e) {
debugger
}
}
)
watch
watch
API 完全等同于组件侦听器 property。watch
需要侦听特定的数据源,并在回调函数中执行副作用。默认情况下,它也是惰性的,即只有当被侦听的源发生变化时才执行回调
与 watchEffect 比较,watch
允许我们:
- 懒执行副作用;
- 更具体地说明什么状态应该触发侦听器重新运行;
- 访问侦听状态变化前后的值。
侦听单个数据源
侦听器数据源可以是返回值的 getter 函数(ref的属性),也可以直接是 ref
// 侦听一个 getter
const state = reactive({ count: 0 })
watch(
() => state.count,
(count, prevCount) => {
/* ... */
}
)
// 直接侦听ref
const count = ref(0)
watch(count, (count, prevCount) => {
/* ... */
})
侦听多个数据源
使用数组同时侦听多个源
const firstName = ref('')
const lastName = ref('')
watch([firstName, lastName], (newValues, prevValues) => {
console.log(newValues, prevValues)
})
firstName.value = 'John' // logs: ["John", ""] ["", ""]
lastName.value = 'Smith' // logs: ["John", "Smith"] ["John", ""]
如果你在同一个方法里同时改变这些被侦听的来源,侦听器仍只会执行一次:(多个同步更改只会触发一次侦听器
)
setup() {
const firstName = ref('')
const lastName = ref('')
watch([firstName, lastName], (newValues, prevValues) => {
console.log(newValues, prevValues)
})
const changeValues = () => {
firstName.value = 'John'
lastName.value = 'Smith'
// 打印 ["John", "Smith"] ["", ""]
}
return { changeValues }
}
通过更改设置 flush: 'sync'
,我们可以为每个更改都强制触发侦听器
这通常是不推荐的。或者,可以用 nextTick 等待侦听器在下一步改变之前运行
const changeValues = async () => {
firstName.value = 'John' // 打印 ["John", ""] ["", ""]
await nextTick()
lastName.value = 'Smith' // 打印 ["John", "Smith"] ["John", ""]
}
侦听响应式对象
使用侦听器来比较一个数组或对象的值
const numbers = reactive([1, 2, 3, 4])
watch(
() => [...numbers],
(numbers, prevNumbers) => {
console.log(numbers, prevNumbers)
}
)
numbers.push(5) // logs: [1,2,3,4,5] [1,2,3,4]
尝试检查深度嵌套对象或数组中的 property 变化时,仍然需要 deep
选项设置为 true
const state = reactive({
id: 1,
attributes: {
name: '',
}
})
watch(
() => state,
(state, prevState) => {
console.log('not deep', state.attributes.name, prevState.attributes.name)
}
)
watch(
() => state,
(state, prevState) => {
console.log('deep', state.attributes.name, prevState.attributes.name)
},
{ deep: true }
)
state.attributes.name = 'Alex' // 日志: "deep" "Alex" "Alex"
Vue 2 中的更改检测警告
由于 JavaScript 的限制,有些 Vue 无法检测的更改类型
对于对象
Vue 无法检测到 property 的添加或删除。由于 Vue 在实例初始化期间执行 getter/setter 转换过程,因此必须在
data
对象中存在一个 property,以便 Vue 对其进行转换并使其具有响应式。Vue 不允许动态添加根级别的响应式 property。
使用 Vue.set(object, propertyName, value)
方法向嵌套对象添加响应式 property:
Vue.set(vm.someObject, 'b', 2)
可以使用 vm.$set
实例方法,这也是全局 Vue.set
方法的别名:
this.$set(this.someObject, 'b', 2)
对于数组
Vue 不能检测以下数组的变动:
- 当你利用索引直接设置一个数组项时,例如:
vm.items[indexOfItem] = newValue
- 当你修改数组的长度时,例如:
vm.items.length = newLength
Vue.nextTick(callback)
回调函数将在 DOM 更新完成后被调用
Vue.component('example', {
template: '<span>{{ message }}</span>',
data: function() {
return {
message: 'not updated'
}
},
methods: {
updateMessage: function() {
this.message = 'updated'
console.log(this.$el.textContent) // => 'not updated'
this.$nextTick(function() {
console.log(this.$el.textContent) // => 'updated'
})
}
}
})
因为 $nextTick()
返回一个 Promise 对象,所以你可以使用新的 ES2017 async/await 语法完成相同的事情:
methods: {
updateMessage: async function () {
this.message = 'updated'
console.log(this.$el.textContent) // => 'not updated'
await this.$nextTick()
console.log(this.$el.textContent) // => 'updated'
}
}