不同于面向对象编程(OOP)中通过抽象出各种对象并注重其间的解耦问题等,函数式编程(FP) 聚焦最小的单项操作,将复杂任务变成一次次 f(x) = y
式的函数运算叠加。函数是 FP 中的一等公民(First-class object),可以被当成函数参数或被函数返回;同时,这些函数应该不依赖或影响外部状态,这意味着对于给定的输入,将产生相同的输出。
在 Vue 中,一个函数式组件(FC - functional component)就意味着一个没有实例(没有 this
上下文、没有生命周期方法、不监听任何属性、不管理任何状态)的组件。从外部看,它也可以被视作一个只接受一些 prop 并按预期返回某种渲染结果的 fc(props) => VNode
函数。Vue 中的 FC 有时也被称作无状态组件(stateless component)。
v-if
等指令,使用 h 函数或结合 jsx 更容易地实现子组件的条件性渲染
+ v-if 指令
更容易地实现高阶组件(HOC - higher-order component)模式,即一个封装了某些逻辑并条件性地渲染参数子组件的容器组件真正的 FP 函数基于不可变状态(immutable state),但 Vue 中的“函数式”组件没有这么理想化。后者基于可变数据,相比普通组件也只是没有实例概念而已。
同时,与 React Hooks 类似的是,Vue Composition API 也在一定程度上为函数式组件带来了少许响应式特征、onMounted 等生命周期式的概念和管理副作用的方法。
无论是 React 还是 Vue,本身都提供了一些验证 props 类型的手段。但这些方法一来配置上都稍显麻烦,二来对于轻巧的函数式组件都有点过“重”了。
TypeScript 作为一种强类型的 JavaScript 超集,可以被用来更精确的定义和检查 props 的类型、使用更简便,在 VSCode 或其他支持 Vetur 的开发工具中的自动提示也更友好。
在 React 中,可以 使用 FC
来约束一个返回了 jsx 的函数入参:
import React
也可以直接定义函数的参数类型,这样的好处是可以对 props 的类型再使用泛型:
interface IGreeting
而 Vue 中的做法该如何呢?
本文主要基于 vue 2.x
版本,结合 tsx 语法,尝试探讨一种在大多数现有 vue 项目中马上就能用起来的、具有良好 props 类型约束的函数式组件实践。
RenderContext 类型被用来约束 render 函数的第二个参数,vue 2.x 项目中对渲染上下文的类型定义如下:
// types/options.d.ts
这很清晰地对应了文档中的相应说明段落:
...组件需要的一切都是通过
context
参数传递,它是一个包括如下字段的对象:
props
:提供所有 prop 的对象children
:VNode 子节点的数组slots
:一个函数,返回了包含所有插槽的对象scopedSlots
:(2.6.0+) 一个暴露传入的作用域插槽的对象。也以函数形式暴露普通插槽。data
:传递给组件的整个数据对象,作为 createElement
的第二个参数传入组件parent
:对父组件的引用listeners
:(2.3.0+) 一个包含了所有父组件为当前组件注册的事件监听器的对象。这是 data.on
的一个别名。injections
:(2.3.0+) 如果使用了 inject
选项,则该对象包含了应当被注入的 property。正如 interface RenderContext
定义的那样,对于函数式组件外部输入的 props,可以使用一个自定义的 TypeScript 接口声明其结构,如:
interface IProps {
而后指定该接口为 RenderContext 的首个泛型:
import Vue, { CreateElement, RenderContext }
在函数式组件中是没有实例上的 this.$emit
可以用的,要达到同样的效果,可以采用下面的写法:
(h: CreateElement, context: RenderContext) => {
配合上 model: { prop, event }
组件选项,对外依然可以达到 v-model
的效果。
在 jsx 返回结构中,传统模板中的 { title | withColon }
过滤器语法不再奏效。
等效的写法比如:
import filters
jsx 中 v-model
指令是无法正确的工作的,替代写法为:
model={{
value: formdata.iptValue,
callback: (v: string) => (formdata.iptValue = v)
}}
placeholder="请填写"
/>
传统模板中对于作用域插槽的用法如下:
jsx 中相应写法则是:
list={attrs}
scopedSlots={{
default: (scope: any) => (
model={{
value: attrs[scope.scopeIndex].attr,
callback: (v: string) => {
//...
}
}}
/>
)
}}
/>
同时,正如例子中所示,element-ui 等全局注册的组件仍需要使用 kebab-case 形式才能正确被编译。
虽说目的是简单渲染的函数式组件中不用太多响应式特性,但也并非不可以一起工作,比如:
import {
了解过以上这些要点,编写一个类型良好的 tsx 函数式组件就没有什么障碍了。
一个实例如: