vue3 tsx 写法下,一个有趣的、基础的渲染问题

下面是一个很常见的 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 并让