如果你需要使用 setup + jsx + 暴露组件内部属性

一般来说会在 render 函数里编写 jsx 代码并返回,在 setup 中返回需要用到的属性

下面例子中的 child.tsx 为了满足上面需求,setup 暴露(返回)了所有方法

如果我们需要将 child.tsxreset,init 方法设为私有呢?

好像行不通,渲染函数只能拿到 setup 中返回的内容,此时会出现一个问题:

  • 渲染函数需要的变量均需要在 setup 中返回,导致无法控制组件暴露给外部的变量

你可能会想到 setup 中返回 jsx 渲染函数,并使用 expose 暴露需要暴露的东西

但是这个方法有一个问题,使用 expose 后组件实例中以 $ 开头的成员会被隐藏,导致很多高级的组件用法无法使用

例如如下代码:

1
2
3
4
5
6
7
8
9
10
11
// test.tsx
import { ref } from 'vue';
export default {
setup(props, { expose }) {
const testData = ref(233);
expose({
testData,
});
return () => <div>{testData}</div>;
},
};

外部通过 <Test ref={testRef}></Test> 拿到组件实例 testRef,其内容如下

1
const testRef = { testData: 233 }; // 组件上的 $el, $attrs 等等都拿不到了

那么有无两全其美的方法呢?

useRender 会给出答案

思路

调试 vue 源码亿下,找到 setup 的实现,跟踪到 handleSetupResult 方法,发现其逻辑如下:

1
2
3
4
5
6
7
8
9
// handleSetupResult

// 如果 setup 执行结果是函数,则将其作为当前 vm 实例的渲染函数
if (isFunction(setupResult)) {
instance.render = setupResult;
// 如果是对象,则解包后放到实例的 setupState 中存好
} else if (isObject(setupResult)) {
instance.setupState = proxyRefs(setupResult);
}

然后传递 vm 实例到方法 finishComponentSetup 中进行处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// finishComponentSetup

// 先拿到组件对象
// PS: instance.type 上放的是组件对象,感兴趣可以自己翻源码
const Component = instance.type;
// 如果当前 vm 实例上没有 render 方法
if (!instance.render) {
// 如果组件对象中没有渲染函数
if (!Component.render) {
// 如果组件实例有模板,则将模板编译为渲染函数,塞给组件对象
if (Component.template) Component.render = compile(Component.template);
}
}
// 将渲染函数塞给 vm 实例
// PS: NOOP 是一个空函数
instance.render = Component.render || NOOP;

最后会在 renderComponentRoot 方法中调用 instance.render 渲染组件

1
2
3
// renderComponentRoot

instance.render!.call(...);

简单来说就是:

  • vm 实例上有 render 就拿来用

  • 没有就找组件上的 render,组件上没有 render 就拿组件模板编译成 render

  • 都没有那就渲染失败报错呗

到这里思路应该就很清晰了

如果我们setup 调用的过程中构建 render 并塞给当前 vm 实例,就可以实现在只暴露部分公开成员的同时渲染函数能通过闭包拿到所有私有成员

实现

简单暴力,但有效

1
2
3
4
5
6
7
8
import { getCurrentInstance } from 'vue';
import type { VNode } from 'vue';

export function useRender(render: () => VNode): void {
// 直接获得当前 vm 实例并给 render 赋值
const vm = getCurrentInstance() as any;
vm.render = render;
}

从类型提示可以发现 render 并不是公开的,所以说该方法偏 Hook,说不准往后一次主版本号更新就寄了

不过既然 vuetify 这种知名项目都在使用该方法,就不用太担心了

使用

基于上面提到的 child.tsx 组件,为了方便对比源码我先贴下来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { defineComponent, ref } from 'vue';

export default defineComponent({
setup() {
const title = ref('title');
const reset = () => (title.value = '');
const init = () => (title.value = 'title');
// 返回了所有成员
return {
title,
reset,
init,
};
},
render() {
return (
<div style={{ border: '1px solid black', padding: '10px' }}>
I am child.
<h1>{this.title}</h1>
<button onClick={this.reset}>reset</button>
<button onClick={this.init}>init</button>
</div>
);
},
});

我们将其改造成只暴露 title 成员

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { defineComponent, ref } from 'vue';

export default defineComponent({
setup() {
const title = ref('title');
const reset = () => (title.value = '');
const init = () => (title.value = 'title');
// 渲染函数通过闭包能拿到所有成员
useRender(() => (
<div style={{ border: '1px solid black', padding: '10px' }}>
I am child.
<h1>{this.title}</h1>
<button onClick={this.reset}>reset</button>
<button onClick={this.init}>init</button>
</div>
));
// 只返回了 title
return {
title,
};
},
});

此时父组件拿到的 Child 实例如下

1
2
3
4
5
6
7
const childRef = {
title, // 只有 title 了捏
$, // 其它组件实例成员也拿得到了捏
$attrs,
$el,
...
}

完美