【翻】Render Functions & JSX

为了提高英语水平和保持技术成长,开始按计划翻译一些短篇和博客,有问题欢迎讨论👻
原文:Render Functions & JSX

正文

Vue 推荐大多数情况下使用模板来构建应用。不过,在某些情况下,我们需要使用JavaScript的全部编程能力。这时,我们就需要使用渲染函数。

Basic Usage

Creating Vnodes

Vue提供了h()函数用于创建vnode

import { h } from 'vue'

const vnode = h(
  'div', // type
  { id: 'foo', class: 'bar' }, // props
  [
    /* children */
  ]
)

h()hyperscript 的缩写,意思是”生成HTML(超文本标记语言)的JavaScript”。这个名字来自于许多虚拟DOM实现时的共同约定。一个描述性更强的名称可以为createVnode(),但当你需要在渲染函数中多次调用时,一个更简短的名称会有所帮助。

这个h()函数设计的非常灵活

// all arguments except the type are optional
h('div')
h('div', { id: 'foo' })

// both attributes and properties can be used in props
// Vue automatically picks the right way to assign it
h('div', { class: 'bar', innerHTML: 'hello' })

// props modifiers such as `.prop` and `.attr` can be added
// with `.` and `^` prefixes respectively
h('div', { '.name': 'some-name', '^width': '100' })

// class and style have the same object / array
// value support that they have in templates
h('div', { class: [foo, { bar }], style: { color: 'red' } })

// event listeners should be passed as onXxx
h('div', { onClick: () => {} })

// children can be a string
h('div', { id: 'foo' }, 'hello')

// props can be omitted when there are no props
h('div', 'hello')
h('div', [h('span', 'hello')])

// children array can contain mixed vnodes and strings
h('div', ['hello', h('span', 'hello')])

生成的vnode结构如下:

const vnode = h('div', { id: 'foo' }, [])

vnode.type // 'div'
vnode.props // { id: 'foo' }
vnode.children // []
vnode.key // null

Note
完整的VNode结构还包含许多其他内部属性,当强烈建议避免依赖以上所列的以外其他属性。这样可以避免在内部属性发生变化时出现意外中断。

Declaring Render Functions

在通过 Composition API 使用模版时,setup()的返回值是用于向模版暴露数据。但在使用渲染函数时,我们可以直接返回渲染函数:

import { ref, h } from 'vue'

export default {
  props: {
    /* ... */
  },
  setup(props) {
    const count = ref(1)

    // return the render function
    return () => h('div', props.msg + count.value)
  }
}

因为渲染函数是在setup()内部声明的,所以它自然可以访问其同一作用域下声明的props和任意的响应数据。

除了返回单个vnode外,还可以返回字符串或者数组:

export default {
  setup() {
    return () => 'hello world!'
  }
}
import { h } from 'vue'

export default {
  setup() {
    // use an array to return multiple root nodes
    return () => [
      h('div'),
      h('div'),
      h('div')
    ]
  }
}

TIP
确保返回的是一个函数,而不是直接返回值!因为每个组件的setup()只会调用一次,而返回的渲染函数会被调用多次。

如果渲染函数不需要任何实例状态,你也可以直接声明一个函数:

function Hello() {
  return 'hello world!'
}

没错,这是一个有效的Vue组件!有关此语法的更多详情,可以查看 Functional Components

Vnodes Must Be Unique

组件树中的所有vnode必须是唯一的。下面的渲染函数就意味着是无效的:

function render() {
  const p = h('p', 'hi')
  return h('div', [
    // Yikes - duplicate vnodes!
    p,
    p
  ])
}

如果你真的想多次复制相同的元素/组件,可以使用工厂函数来实现。比如,下面的渲染函数就是渲染了20个相同内容的有效方法:

function render() {
  return h(
    'div',
    Array.from({ length: 20 }).map(() => {
      return h('p', 'hi')
    })
  )
}

JSX / TSX​

JSX是JavaScript的XML扩展,可以让我们写出以下代码:

const vnode = <div>hello</div>

在JSX表达式中,使用花括号嵌入动态值:

const vnode = <div id={dynamicId}>hello, {userName}</div>

create-vue和Vue CLI都有在脚手架配置项中进行了JSX支持。如果你要手动配置JSX,请参阅@vue/babel-plugin-jsx文档了解详情。

虽然JSX最初是由React引入的,但它实际上并没有定义运行时语义,可以编译成各种不同的输出。如果你以前使用过JSX,需注意Vue的JSX转换和React的JSX转换是不同的,所以你不能在Vue应用中使用React的JSX转换,具体与React JSX的一些明显区别包括:

  • 你可以使用classfor等HTML属性作为props,无需使用className或者htmlFor
  • 将子元素传递给组件(即插槽)的工作方式有所不同。

Vue的类型定义还为TSX的使用提供了类型推断。使用TSX时,确保在tsconfig.json中指定"jsx": "preserve",以便在TypeScript保留JSX语法,供Vue JSX转换处理。

JSX Type Inference

与transform一样,Vue的JSX也需要不同的类型定义。目前,Vue会自动全局注册Vue的JSX类型。这意味着,当Vue的类型可用时,TSX就能开箱即用。

Vue的全局JSX类型可能会与其他同样需要JSX类型推导的库发生冲突,特别是React。从3.3版本开始,Vue支持通过TypeScript的jsxImportSource选项指定JSX命名空间。我们计划在3.4版本中移除默认的全局JSX命名空间注册。

对于TSX用户,建议在升级到3.3之后在 tsconfig.json 中将 jsxImportSource 选项设置为 “vue”,或者在每个文件中使用 /* @jsxImportSource vue */。这样,你现在就可以选择加入新行为,并在 3.4 发布时无缝升级。

如果有代码依赖于全局JSX命名空间,你可以通过显式引用 vue/jsx(它会注册全局 JSX 命名空间)来保留完全相同的 3.4 之前的全局行为

Render Function Recipes

下面我们会提供一些用等价的渲染函数/JSX实现模版功能的常用方法。

v-if

template:

<div>
  <div v-if="ok">yes</div>
  <span v-else>no</span>
</div>

等价 渲染函数/JSX:

h('div', [ok.value ? h('div', 'yes') : h('span', 'no')])
<div>{ok.value ? <div>yes</div> : <span>no</span>}</div>

v-for

template:

<ul>
  <li v-for="{ id, text } in items" :key="id">
    {{ text }}
  </li>
</ul>

等价 渲染函数/JSX:

h(
  'ul',
  // assuming `items` is a ref with array value
  items.value.map(({ id, text }) => {
    return h('li', { key: id }, text)
  })
)
<ul>
  {items.value.map(({ id, text }) => {
    return <li key={id}>{text}</li>
  })}
</ul>

v-on

props名称以on开头且后面跟大写字母的被视为事件监听器。比如,onClick相当于模版的@click

h(
  'button',
  {
    onClick(event) {
      /* ... */
    }
  },
  'click me'
)
<button
  onClick={(event) => {
    /* ... */
  }}
>
  click me
</button>

Event Modifiers

对于.passive.capture.once事件修饰符,可以驼峰写法在事件名称后面进行连接。

例如:

h('input', {
  onClickCapture() {
    /* listener in capture mode */
  },
  onKeyupOnce() {
    /* triggers only once */
  },
  onMouseoverOnceCapture() {
    /* once + capture */
  }
})
<input
  onClickCapture={() => {}}
  onKeyupOnce={() => {}}
  onMouseoverOnceCapture={() => {}}
/>

然后其余的事件和按键修饰符,可以使用withModifiers协助使用:

import { withModifiers } from 'vue'

h('div', {
  onClick: withModifiers(() => {}, ['self'])
})
<div onClick={withModifiers(() => {}, ['self'])} />

Components

要为组件创建vnode,传递给h()的第一个参数应该是组件定义。这就意味着使用渲染函数时,无需注册组件,只是直接使用导入的组件即可:

import Foo from './Foo.vue'
import Bar from './Bar.jsx'

function render() {
  return h('div', [h(Foo), h(Bar)])
}
function render() {
  return (
    <div>
      <Foo />
      <Bar />
    </div>
  )
}

正如我们看到的,只要是有效的Vue组件,h函数就能处理任何文件格式导入的组件。

动态组件在渲染组件中可直接使用:

import Foo from './Foo.vue'
import Bar from './Bar.jsx'

function render() {
  return ok.value ? h(Foo) : h(Bar)
}
function render() {
  return ok.value ? <Foo /> : <Bar />
}

如果组件是用名称注册的,无法直接导入(例如,有库进行的全局注册),这种情况可以使用resolveComponent来解析该组件。

Rendering Slots

在渲染函数中,可以通过setup()的上下文访问slot。然后slots对象上的每一个slot都是一个函数,会返回一个vnodes数组:

export default {
  props: ['message'],
  setup(props, { slots }) {
    return () => [
      // default slot:
      // <div><slot /></div>
      h('div', slots.default()),

      // named slot:
      // <div><slot name="footer" :text="message" /></div>
      h(
        'div',
        slots.footer({
          text: props.message
        })
      )
    ]
  }
}

JSX:

// default
<div>{slots.default()}</div>

// named
<div>{slots.footer({ text: props.message })}</div>

Passing Slots

将子元素传递给组件和子元素传递给元素的方式有些不同。我们不需要传递数组,而是传递slot函数或者slot函数对象。slot函数可以返回与普通渲染函数的返回值一样的内容,并且在子组件访问时,这些内容将始终会转换为一个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:

// default
<MyComponent>{() => 'hello'}</MyComponent>

// named
<MyComponent>{{
  default: () => 'default slot',
  foo: () => <div>foo</div>,
  bar: () => [<span>one</span>, <span>two</span>]
}}</MyComponent>

将slot作为函数传递给子组件,可以让子组件懒散地调用slot。这样,子组件而不是父组件就能跟踪slot的依赖关系,从而实现更准确、更有效的更新。

Built-in Components

内置组件如<KeepAlive>, <Transition>, <TransitionGroup>, <Teleport><Suspense>必须导入才能在渲染函数中使用

import { h, KeepAlive, Teleport, Transition, TransitionGroup } from 'vue'

export default {
  setup () {
    return () => h(Transition, { mode: 'out-in' }, /* ... */)
  }
}

v-model

在模版编译过程中,v-model指令会扩展为modelValueonUpdate:modelValue两个props,在渲染函数中就必须自己提供这些props:

export default {
  props: ['modelValue'],
  emits: ['update:modelValue'],
  setup(props, { emit }) {
    return () =>
      h(SomeComponent, {
        modelValue: props.modelValue,
        'onUpdate:modelValue': (value) => emit('update:modelValue', value)
      })
  }
}

Custom Directives

自定义指令可以通过withDirectives应用到vnode:

import { h, withDirectives } from 'vue'

// a custom directive
const pin = {
  mounted() { /* ... */ },
  updated() { /* ... */ }
}

// <div v-pin:top.animate="200"></div>
const vnode = withDirectives(h('div'), [
  [pin, 200, 'top', { animate: true }]
])

如果指令是通过名字注册的,是无法直接导入的,需要使用resolveDirective对其进行解析。

Template Refs

在使用组合式API时,模版ref是通过ref()本身作为props传递给vnode创建的

import { h, ref } from 'vue'

export default {
  setup() {
    const divEl = ref()

    // <div ref="divEl">
    return () => h('div', { ref: divEl })
  }
}

Functional Components

函数组件是组件的另一种形式,自身没有任何状态。执行的内容就像纯函数,props输入,vnode输出。在渲染时不创建组件实例(即没有this),也没有一些组件声明周期钩子。

我们会通过一个普通函数来创建一个函数组件,而不是一个选项对象。该函数实际上就是组件的渲染函数。

函数组件的签名与setup()hook相同:

function MyComponent(props, { slots, emit, attrs }) {
  // ...
}

组件的大多数常规配置选项都不适用于函数组件。不过,可以通过添加属性来定义propsemits

MyComponent.props = ['value']
MyComponent.emits = ['click']

如果未指定props选项,那么传递给函数的props对象将包含所有属性,与attrs相同。除非指定来props选项,否则props名称不会规范为驼峰。

对于没有明确props的函数组件,属性穿透的工作原理与普通组件大致相同。但是,对于没有明确指定props的函数组件,默认情况下将会从attrs继承classstyleonXxx事件侦听器。无论是哪种情况,都可以将inheritAttrs设为false以禁用属性继承:

MyComponent.inheritAttrs = false

函数组件可以像普通组件一样注册和使用。如果将函数作为h()的第一个参数,它会被视为函数组件。

Typing Functional Components

函数组件可根据其命名或匿名类型来进行标注类型。在SFC模板中使用时,Volar还支持对其进行类型检查。

具名函数组件

import type { SetupContext } from 'vue'
type FComponentProps = {
  message: string
}

type Events = {
  sendMessage(message: string): void
}

function FComponent(
  props: FComponentProps,
  context: SetupContext<Events>
) {
  return (
    <button onClick={() => context.emit('sendMessage', props.message)}>
        {props.message} {' '}
    </button>
  )
}

FComponent.props = {
  message: {
    type: String,
    required: true
  }
}

FComponent.emits = {
  sendMessage: (value: unknown) => typeof value === 'string'
}

匿名函数组件

import type { FunctionalComponent } from 'vue'

type FComponentProps = {
  message: string
}

type Events = {
  sendMessage(message: string): void
}

const FComponent: FunctionalComponent<FComponentProps, Events> = (
  props,
  context
) => {
  return (
    <button onClick={() => context.emit('sendMessage', props.message)}>
        {props.message} {' '}
    </button>
  )
}

FComponent.props = {
  message: {
    type: String,
    required: true
  }
}

FComponent.emits = {
  sendMessage: (value) => typeof value === 'string'
}