• 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>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    页面的显示会在 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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    直觉上这会正常。但细想就知道肯定不对了。
    来分析一下渲染的流程:
    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>
    
    • 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
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    这个例子中,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>
      }
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    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);
      },
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    上述两种写法是等价的。亦即 vue3 会判断返回的是一个 function 还是 object。如果是 object 会形成一个上下文 this,交给 render 进行 vNodes 生成。

    注意,有时候我们会习惯文件按照 SFC 写法,同时使用 tsx 的 return vodes 并让