vue3学习小札之(五):渲染函数 & JSX
引子
可以前往专栏阅读该系列其他文章:传送门
vue 作为一个前端框架,定义了一套模板语法
,本篇文章将学习 vue 中涉及到模板渲染的相关知识。
渲染机制
vue 中的模板语法,现代浏览器并不能识别,所以需要将模板转换为真实的 DOM 节点,而这一切的基础,就是虚拟 DOM
。
虚拟 DOM
虚拟 DOM (Virtual DOM,简称 VDOM)
是一种编程概念,意为将目标所需的 UI 通过数据结构“虚拟”地表示出来,保存在内存中,然后将真实的 DOM 与之保持同步。
下面就是一个简单的虚拟 DOM:
const vnode = {
type: 'div',
props: {
id: 'hello'
},
children: [
/* 更多 vnode */
]
}
这里所说的 vnode
即一个纯 JavaScript 的对象
(一个“虚拟节点”),它代表着一个 <div>
元素。
它包含我们创建实际元素所需的所有信息。
它还包含更多的子节点
,这使它成为虚拟 DOM 树的根节点
。
vue 的渲染机制中,涉及到如下几个过程: 编译:Vue 模板被编译为了渲染函数:即用来返回虚拟 DOM 树的函数;
挂载 (mount):遍历整个虚拟 DOM 树,并据此构建真实的 DOM 树;
这一步会作为响应式副作用执行,因此它会追踪其中所用到的所有响应式依赖
。vue 的数据响应式,就是在这一过程中初始化的。
更新 (patch):当一个依赖发生变化后,副作用会重新运行,这时候会创建一个更新后的虚拟 DOM 树。运行时渲染器遍历这棵新树,将它与旧树进行比较
,然后将必要的更新应用到真实 DOM 上去。又被称为“比对”(diffing),最核心的就是 diff 算法
;
模板 vs. 渲染函数
上面提到了一个概念叫做:渲染函数
。Vue 模板会被预编译成虚拟 DOM 渲染函数。
vue 模板的优势
:
模板更贴近实际的 HTML。这使得我们能够更方便地重用一些已有的 HTML 代码片段,能够带来更好的可访问性体验、能更方便地使用 CSS 应用样式,并且更容易
使设计师理解和修改。
由于其确定的语法,更容易对模板做静态分析。这使得 Vue 的模板编译器能够应用许多编译时优化
来提升虚拟 DOM 的性能
表现。
渲染函数的优势
:
在处理高度动态
的逻辑时,渲染函数相比于模板更加灵活
,因为你可以完全地使用 JavaScript 来构造你想要的 vnode(虚拟 DOM 节点)。
渲染函数
接下来,学习如何编写渲染函数:
基本用法
Vue 提供了一个 h()
函数用于创建 vnodes:
创建 Vnodes
import { h } from 'vue'
const vnode = h(
'div', // type
{ id: 'foo', class: 'bar' }, // props
[
/* children */
]
)
h()
函数的使用方式非常的灵活:
除了类型必填
以外,其他的参数都是可选的:
h('div')
h('div', { id: 'foo' })
attribute 和 property 都能在 prop 中书写,Vue 会自动将它们分配到正确的位置:
h('div', { class: 'bar', innerHTML: 'hello' })
props 修饰符和属性传递(例如.prop和.attr)可以分别使用'.'和'^'前缀:
h('div', { '.name': 'some-name', '^width': '100' })
类与样式可以像在模板中一样用数组或对象的形式书写:
h('div', { class: [foo, { bar }], style: { color: 'red' } })
事件监听器应以 onXxx
的形式书写:
h('div', { onClick: () => {} })
children 可以是一个字符串:
// 此时子元素就是一个文本元素
h('div', { id: 'foo' }, 'hello')
没有 props 时可以省略不写:
// 因为 props 必须是一个对象,所以当第二个参数传入的类型不是对象时
// vue 会自动将其归为第三个参数:children
h('div', 'hello') h('div', [h('span', 'hello')])
children 数组可以同时包含 vnodes 与字符串:
h('div', ['hello', h('span', 'hello')])
声明渲染函数
当组合式 API 与模板一起使用时,setup()
钩子的返回值是用于暴露数据给模板。
然而当我们使用渲染函数时,可以直接把渲染函数返回
。
在 setup()
内部声明的渲染函数天生能够访问在同一范围内声明的 props 和许多响应式状态。
import { ref, h } from 'vue'
export default {
props: {
/* ... */
},
setup(props) {
const count = ref(1)
// 返回渲染函数
return () => h('div', props.msg + count.value)
}
}
export default {
setup() {
// 使用数组返回多个根节点
return () => [
h('div'),
h('div'),
h('div')
]
}
}
注意:
请确保返回的是一个函数而不是一个值!setup()
函数在每个组件中只会被调用一次,而返回的渲染函数将会被调用多次,因为渲染是存在更新
的。
Vnodes 必须唯一
组件树中的 vnodes 必须是唯一
的。
错误的写法:
function render() {
const p = h('p', 'hi')
return h('div', [
// 啊哦,重复的 vnodes 是无效的
p,
p
])
}
正确的写法
:
使用一个工厂函数
function render() {
return h(
'div',
Array.from({ length: 20 }).map(() => {
return h('p', 'hi')
})
)
}
JSX / TSX
JSX 是 JavaScript 的一个类似 XML 的扩展。
vue 中除了可以通过渲染函数来定义一个虚拟 DOM 节点,还可以使用 JSX:
// 使用大括号来嵌入动态值
const vnode = <div id={dynamicId}>hello, {userName}</div>
Vue 的类型定义也提供了 TSX 语法的类型推导支持。当使用 TSX 语法时,确保在 tsconfig.json
中配置了 "jsx": "preserve"
,这样的 TypeScript 就能保证 Vue JSX 语法编译过程中的完整性。
渲染函数案例
可以前往官网查看如何用渲染函数 / JSX 语法,实现等价的模板功能。
这里只做简单的总结
:
ref 响应式变量在模板语法中存在解包,但是在渲染函数和 JSX 是不存在解包
的,所以需要 .value
。
以 on
开头,并跟着大写字母的 props 会被当作事件监听器。比如,onClick
与模板中的 @click
等价。
对于 .passive
、.capture
和 .once
事件修饰符,可以使用驼峰写法将他们拼接在事件名后面。
对于其他的事件和按键修饰符,可以使用 withModifiers
函数。
如果一个组件是用名字注册的,不能直接导入 (例如,由一个库全局注册),可以使用 resolveComponent()
来解决这个问题。
诸如 <KeepAlive>
、<Transition>
、<TransitionGroup>
、<Teleport>
和 <Suspense>
等内置组件在渲染函数中必须导入才能使用
插槽相关
组件内渲染插槽
在渲染函数中,插槽可以通过 setup()
的上下文来访问。
每个 slots
对象中的插槽都是一个返回 vnodes 数组的函数:
export default {
props: ['message'],
setup(props, { slots }) {
return () => [
// 因为插槽本身就是一个返回虚拟 dom 节点数组的函数
// 所以可以直接调用 作为参数传入
// 默认插槽:
// <div><slot /></div>
h('div', slots.default()),
// 具名插槽:
// <div><slot name="footer" :text="message" /></div>
h(
'div',
slots.footer({
text: props.message
})
)
]
}
}
等价 JSX 语法:
// 默认插槽
<div>{slots.default()}</div>
// 具名插槽
<div>{slots.footer({ text: props.message })}</div>
消费组件传递插槽
我们需要传递一个插槽函数
或者是一个包含插槽函数的对象
而非是数组,插槽函数的返回值同一个正常的渲染函数的返回值一样,都是一个虚拟 dom 节点。
并且某个插槽函数在子组件中被访问时返回值总是会被转化为一个 vnodes 数组
。
// 单个默认插槽
h(MyComponent, () => 'hello')
// 具名插槽
// 注意 `null` 是必需的
// 以避免 slot 对象被当成 prop 处理
h(MyComponent, null, {
default: () => 'default slot',
foo: () => h('div', 'foo'),
bar: () => [h('span', 'one'), h('span', 'two')]
})
等价 JSX 语法:
// 默认插槽
<MyComponent>{() => 'hello'}</MyComponent>
// 具名插槽
<MyComponent>{{
default: () => 'default slot',
foo: () => <div>foo</div>,
bar: () => [<span>one</span>, <span>two</span>]
}}</MyComponent>
v-model
v-model
指令扩展为 modelValue
和 onUpdate:modelValue
在模板编译过程中,我们必须自己提供这些 props:
export default {
props: ['modelValue'],
emits: ['update:modelValue'],
setup(props, { emit }) {
return () =>
h(SomeComponent, {
modelValue: props.modelValue,
'onUpdate:modelValue': (value) => emit('update:modelValue', value)
})
}
}
自定义指令
可以使用 withDirectives
将自定义指令应用于 vnode:
import { h, withDirectives } from 'vue'
// 自定义指令
const pin = {
mounted() { /* ... */ },
updated() { /* ... */ }
}
// <div v-pin:top.animate="200"></div>
const vnode = withDirectives(h('div'), [
[pin, 200, 'top', { animate: true }]
])
当一个指令是以名称注册并且不能被直接导入时,可以使用 resolveDirective
函数来解决这个问题。
函数式组件
函数式组件是一种定义自身没有任何状态
的组件的方式。
它们很像纯函数:接收 props,返回 vnodes
。
函数式组件在渲染过程中不会创建组件实例 (也就是说,没有 this
),也不会触发常规的组件生命周期钩子。
用一个普通的函数
而不是一个选项对象来创建函数式组件
。
该函数实际上就是该组件的渲染函数
。
函数式组件可以像普通组件一样被注册和使用。
如果将一个函数作为第一个参数传入 h
,它将会被当作一个函数式组件来对待。
函数式组件的签名与 setup()
钩子相同:
function MyComponent(props, { slots, emit, attrs }) {
// ...
}
大多数常规组件的配置选项在函数式组件中都不可用,除了 props
和 emits
。
我们可以给函数式组件添加对应的属性来声明它们:
MyComponent.props = ['value']
MyComponent.emits = ['click']
总结
本篇文章主要介绍了 vue 中跟渲染
相关的知识。
首先是虚拟 DOM
,基于虚拟 DOM 的概念,我们可以编写灵活的渲染函数
。
除了渲染函数,我们还可以通过 vue 定义的 JSX / TSX
语法来定义虚拟 DOM。
转载自:https://juejin.cn/post/7142300147446186014