前端知库

Vue 的渲染函数、JSX 和 h() 怎样使用?

模板最终会编译成渲染函数,渲染函数调用 h() 创建虚拟节点。h(type, props, children) 里 type 可以是标签或组件,事件写成 onXxx;v-if 换成三元表达式,v-for 换成 map,插槽要传函数。Vue 的 JSX 可以直接写 class,插槽同样传函数对象。

参考答案

简答

比如要写一个标题组件,传 level: 2 就渲染 <h2>,传 3 就渲染 <h3>。用模板得写一串 v-if,用渲染函数一行 h('h' + props.level, slots.default()) 就行。

h() 的作用是创建虚拟节点,参数是 h(类型, props, 子节点)。类型可以是标签名,也可以直接传导入的组件对象;props 里 class、style 和模板里一样写,事件写成 onClick 这种 on 加大写字母开头的形式;子节点可以是字符串或节点数组。除了类型,其他参数都能省。

模板指令在渲染函数里都要换成 JavaScript:v-if 用三元表达式,v-for 用 map 并传 key,v-model 拆成 modelValue 和 onUpdate:modelValue。给组件传插槽要传函数,具名插槽写成函数对象,前面的 props 位置没有值时要填 null。JSX 是同一件事的另一种写法,Vue 的 JSX 能直接写 class 和 for。

要注意,在 setup 里要返回一个函数,而不是直接返回 h() 的结果,因为 setup 只执行一次,返回的渲染函数才会在每次更新时重新执行。另外同一个虚拟节点不能在树里用两次,需要重复时用函数每次新建。

模板和渲染函数是什么关系

模板会先被编译成渲染函数,运行时执行渲染函数,得到一棵虚拟节点树,再交给渲染器更新 DOM。编译的过程见《Vue 模板编译的过程是怎样的?parse、transform、generate 各做什么?》。

所以手写渲染函数就是跳过编译,自己写编译器原本会生成的那部分代码。大部分场景模板更好读,编译器还能做静态提升、补丁标记等优化,见《Vue 模板编译如何减少运行时更新工作?》。渲染函数适合逻辑比结构复杂的场景,比如上面按 level 切换标签。

h() 的几种写法

官方文档列出的参数形式如下,除了第一个参数都可以省略:

JavaScript
import { h } from 'vue';

h('div');
h('div', { id: 'foo' });
h('div', { class: 'bar', innerHTML: 'hello' });
h('div', { onClick: () => {} });
h('div', { id: 'foo' }, 'hello');
h('div', 'hello');
h('div', [h('span', 'hello')]);

第二个参数为什么能同时放 class 和 innerHTML?文档说明,HTML 属性和 DOM 属性都可以写在 props 里,Vue 会自动选择正确的设置方式。确实需要强制时,可以加 . 前缀表示 DOM 属性、^ 前缀表示 HTML 属性。

第一个参数传组件时,直接传导入的组件对象,不需要注册:

JavaScript
import Foo from './Foo.vue';

h('div', [h(Foo, { msg: 'hi' })]);

在 setup 里返回渲染函数

组合式 API 里,setup 返回一个函数,这个函数就是渲染函数:

JavaScript
import { ref, h } from 'vue';

export default {
  props: { msg: String },
  setup(props) {
    const count = ref(1);
    return () => h('div', { onClick: () => count.value++ }, props.msg + count.value);
  },
};

为什么不能直接 return h('div', ...)?因为 setup 每个组件实例只执行一次,直接返回节点就只渲染这一次的结果。返回函数以后,Vue 每次更新都调用它,函数里读到的 count.value 和 props.msg 是最新值,也会被收集成依赖。

指令在渲染函数里怎么写

模板指令都要换成普通 JavaScript。下面把 v-if、v-for 和 v-model 放在一起看:

JavaScript
h('div', [
  ok.value ? h('p', 'yes') : h('p', 'no'),
  h(
    'ul',
    items.value.map(({ id, text }) => h('li', { key: id }, text)),
  ),
  h(SomeInput, {
    modelValue: text.value,
    'onUpdate:modelValue': (value) => (text.value = value),
  }),
]);

v-for 换成 map 以后,key 仍然要写,它放在 props 里。组件上的 v-model 本来就是 modelValue 加 update:modelValue 事件的简写,见《组件 v-model 的本质是什么?》,渲染函数里要自己把这两个都传上。

事件修饰符里,.passive、.capture、.once 可以直接拼进事件名,比如 onClickCapture、onKeyupOnce;.self、.stop 这类要用 withModifiers(handler, ['self']) 包一下。

同一个节点能不能复用?文档要求组件树里的虚拟节点必须唯一。想渲染 20 个相同的段落,不能把同一个 h('p', 'hi') 放进数组 20 次,要用 Array.from({ length: 20 }).map(() => h('p', 'hi')) 每次新建。

插槽为什么要传函数

组件内部渲染插槽时,slots 上的每个插槽都是一个函数,调用后返回节点数组:

JavaScript
setup(props, { slots }) {
  return () => [
    h('div', slots.default()),
    h('div', slots.footer({ text: props.message })),
  ];
}

父组件给子组件传插槽,也要传函数而不是节点数组:

JavaScript
h(MyComponent, () => 'hello');

h(MyComponent, null, {
  default: () => 'default slot',
  footer: ({ text }) => h('p', text),
});

第二种写法里的 null 不能省,否则插槽对象会被当成 props。为什么要传函数?文档说明,传函数让子组件调用它,插槽里用到的数据就被子组件追踪成依赖,更新更准确,也更高效。作用域插槽就是子组件调用时传了参数,比如上面的 { text }。插槽本身的概念见《插槽、作用域插槽和 provide/inject 各解决什么问题?》。

Vue 的 JSX 和 React 有什么不同

JSX 是写渲染函数的另一种语法,最后同样编译成创建虚拟节点的调用。项目里通过 create-vue 或 Vue CLI 开启,手动配置用 @vue/babel-plugin-jsx。

JSX
const vnode = (
  <div class="card" onClick={handleClick}>
    {ok.value ? <span>yes</span> : <span>no</span>}
  </div>
);

文档强调 Vue 的 JSX 转换和 React 的不同。第一,可以直接写 class 和 for,不用写 className、htmlFor。第二,给组件传插槽时,子节点要写成函数对象:

JSX
<MyComponent>
  {{
    default: () => 'default slot',
    foo: () => <div>foo</div>,
  }}
</MyComponent>

用 TSX 时,tsconfig.json 要设置 "jsx": "preserve",让 TypeScript 保留 JSX 交给 Vue 的转换处理。Vue 3.4 起不再隐式注册全局 JSX 命名空间,要加 "jsxImportSource": "vue",或者在文件顶部写 /* @jsxImportSource vue */。

常见追问

函数式组件是什么?

就是一个普通函数,接收 props 和上下文,返回虚拟节点,比如 function MyComponent(props, { slots, emit, attrs }) {}。它不创建组件实例,没有 this,也没有常规的生命周期钩子。声明 props 要写在函数上,比如 MyComponent.props = ['value']。

选项式 API 里怎么写渲染函数?

写 render() 选项,在里面用 this 访问组件数据,比如 render() { return h('div', this.message) },插槽通过 this.$slots.default() 渲染。