下面是一个很常见的 tsx 代码片:
<script lang="tsx">
import { defineComponent, ref } from 'vue';
export default defineComponent({
name: 'MyComponent',
setup() {
const a = ref('kkk');
setTimeout(() => {
a.value = 'aaa';
console.log(`change to aaaa!!!`);
}, 3000);
return () => <div>{a.value}</div>;
},
});
</script>
页面的显示会在 3000ms 后从 ‘kkk’ 变 ‘aaa’。
来思考一个问题,类似的以下代码会不会在3000ms后让显示变成 ‘aaa’ 呢?
<script lang="tsx">
import { defineComponent, ref } from 'vue';
function genComponent() {
const a = ref('kkk');
setTimeout(() => {
a.value = 'aaa';
console.log(`change to aaaa!!!`);
}, 3000);
return <div>{a.value}</div>;
}
export default defineComponent({
name: 'MyComponent',
setup() {
return () => genComponent();
},
});
</script>
直觉上这会正常。但细想就知道肯定不对了。
来分析一下渲染的流程:
1、一开始,genComponet 里的 tsx 代码返回了一个响应式显示 a 变量(‘kkk’)的虚拟节点。
2、3000ms 后,a 变量复制了 ‘aaa’,响应式影响了此节点,于是引起页面的更新。
3、页面更新重新调用了 genComponent 函数,新的节点又生成了,并且跟显示是 ‘kkk’ ,回到了第一步。
4、周而复始,导致页面显示的一直是 ‘kkk’,而且函数 genComponent 在 3000ms 不断被调用。
以上就是一个容易被 “闭包” 思维混淆的小例子。
再来一个好玩的例子:
<script lang="tsx">
import { defineComponent, ref } from 'vue';
function genComponent() {
console.log(`genComponent`)
const a = ref('kkk');
setTimeout(() => {
a.value = 'aaa';
console.log(`change to aaaa!!!`);
}, 3000);
return <div>{a.value}</div>;
}
function genComponent2() {
console.log(`genComponent2`)
const a = ref('zzz');
return <div>{a.value}</div>;
}
export default defineComponent({
name: 'MyComponent',
setup() {
return () =>
<div>
<div>
{
genComponent()
}
</div>
<div>
{
genComponent2()
}
</div>
</div>
},
});
</script>
这个例子中,getComponent2 会被 getComponent1 影响而不断被调用吗?
答案是会的。
要理解为什么,得先搞清楚 vue3 的大概编译逻辑。
setup 下 return 回去的其实就是render 函数会执行的 vNodes,而 tsx 无非就是支持了语法编译成等效的 h(…) 逻辑。有些时候为了表达清晰,保持跟 ts 模式的一致性,还会有人选择将这部分分开来写。比如:
import { defineComponent, h, ref } from 'vue';
export default defineComponent({
setup() {
const message = ref('Hello, world!');
console.log(`setup!!`)
return () => <div>{message.value}</div>
}
});
与
import { defineComponent, h, ref } from 'vue';
export default defineComponent({
setup() {
const message = ref('Hello, world!');
console.log(`setup!!`)
return {
message,
};
},
render() {
console.log(`render`)
return h('div', this.message);
},
});
上述两种写法是等价的。亦即 vue3 会判断返回的是一个 function 还是 object。如果是 object 会形成一个上下文 this,交给 render 进行 vNodes 生成。
注意,有时候我们会习惯文件按照 SFC 写法,同时使用 tsx 的 return vodes 并让 标签能生效,以适应 less 等专门的 style 语法。此时 tsx 的 return vnodes 语法依然是可以生效的。但是 render 函数会失效。也就是说,在 SFC 写法下,如果 setup return 回去的是一个 object,就必然不能少掉 block。
可以用以下的代码尝试,去掉 template 观察还能否正常渲染。<template> <div>{{ message }}</div> </template> <script lang="tsx"> import { defineComponent, h, ref } from 'vue'; export default defineComponent({ setup() { const message = ref('Hello, world!'); console.log(`setup!!`) return { message, }; }, render() { console.log(`render`) return h('div', this.message); }, }); </script>
只要 reactive 引起了组件的改动,整个组件 render 函数就会被重新执行一遍。
初学者在写 tsx 的时候会混淆 “局部更新” 的概念,事实上 vue3 的局部更新发生在 vNodes 渲染真实 DOM 的比较算法上。而 Component 组件的 render 函数每次都会重新执行。
如果进一步观察,会发现这个规则只在组件内影响。
父组件、子组件由 reactive 变量引起的 render 重新执行不会引起连带效果。
想一下为啥?
答案很简单。分两个方向看:
子的变化,父自然没有任何自身关联的 reactive 变量影响了页面,所以父组件不会发生 render 重新调用。
而父的变化,父的 render 函数重新执行时,会判断到子组件的 vNodes 没有发生变化,于是也不会进行任何操作。