今天(5月22日),字节跳动充满活力的新闻团队前端工程师林成璋出席《Vue Conf 21》大会,与前端技术爱好者交流,并在大会上发表了题为《Vue Conf 21》的文章。以下是本次分享的全部内容,最后的总结是的重头戏。
简介
大家下午好,同学。我是来自字节跳动前端团队大理智能的林成璋。在我最近六个月的空闲时间(加上一些钓鱼时间),我主要是在维护Vue 3的BabelJSX插件。今天我要和大家分享分享JSX。
以下是我的Github账号,应该是除P站外全网的头像了。其实这个插件最初是为了帮助 Ant Design Vue 和 Vant 快速升级到 Vue 3 而创建的。看过他们源码的同学应该知道他们的大部分源码都是JSX写的。
虽然目前 NPM 上的每周下载量是 56 王牌(甚至比 Vue 3 还多?),但这里下载量非常高的原因主要是通过 vue-cli 创建的项目(无论是 Vue 2 还是 Vue 3),软件包将下载@vue/babel-plugin-jsx
。实际使用JSX的用户数量应该远少于这个数字。JSX发展了多少用户,无法统计。大多数用户仍然根据开发方式使用template
。
基本概念
- 模板
在Vue中,sfc
是以.vue
结尾的文件,通常包含<template>
、♻和三种顶级语言块,可以理解为HTML和J CSS组合。任何以 .vue
结尾的文件都是一个组件,默认情况下只能导出一个组件。
- JSX
本身就是JS
为什么还要在Vue支持JSX
Vue官方推荐的开发方式是template。从 Vue 2 开始,模板在执行之前将被编译成 JavaScript 渲染函数。这些 render function
是传奇的 render function
。
每当我们谈论模板和JSX时,我们都会讨论一个比较大的问题:React
和Vue
哪个更好。有些人可能不喜欢直接用 JavaScript 来表达 UI,但不少人会认为用模板编写会很烦人,尤其是经验丰富的 React 玩家。由于vue是世界上最人性化的UI框架,因此它拥有广泛的群众基础。有些人习惯直接使用 HTML 和 CSS 来创建代码。完全切换并开始思考如何用 JavaScript 创建用户界面要容易得多。但不得不承认,对于一些以前干后端的同学,或者iOS、Android开发者来说,他们从来没有太多接触过HTML,通过字符串模板来写UI并不好。
不同的用户有不同的口味,萝卜白菜,每个人都有自己的喜好。就像这个PPT,有的人看完可能会很激动,有的人可能会觉得我是个傻子。你可以讲很多模板如何糟糕的例子,而且,他给JSX带来了困难,没有人能说服任何人。所以 Vue 两者兼而有之。
“真正的”JSX是什么样的
JSX最初是Facebook设计的规范。下面的X
可以理解为JavaScript语法的扩展。对同学感兴趣的人可以去这个链接查看具体内容。因为每个前端框架的实现都不同,所以不会由引擎或浏览器来实现。改造完之后,还需要转换成普通的JS。我们可以将这一步视为在浏览器中运行之前的“授权”。JSX实际上类似于模板语言但是具有JavaScript的所有功能,但是由于模板的一些限制,使用模板编写的代码的性能比JSX要好得多。
<h1>Hello, world!</h1>;
这里,JSX语法其实是整理出来的:
import { createVNode as _createVNode } from "vue"
_createVNode("h1", null, "Hello, world!");
Vue 3 有何变化
Vue 2 早期是用纯 JavaScript 编写的。随着项目的发展和规模扩大,Facebook 的 Flow 被引入。尽管 Flow 在一定程度上有所帮助,但仍然存在一些问题。游达还公开表示,选择 Flow 而不是 TypeScript 是一个“糟糕的赌注”。
在Vue 2中,JSX的编译必须依赖两个包@vue/babel-preset-jsx
和@vue/babel-helper-vue-jsx-merge-props
。第一个包负责编译JSX语法,第二个包用于实现运行时函数mergeProps
。
但如果想在TSX环境下编写,则需要额外安装vue-tsx-support。
在 Vue 3 中,只需安装 Babel 插件即可。显然,不需要额外的第三方库。源码中有jsx.d.ts支持JSX类型检查
JSX的场景
现在让我们看看哪些场景比模板更优雅地使用JSX。
将多个组件写入一个文件
只能将一个组件写入文件.vue
。老实说,在某些情况下这是不合适的。很多时候我们在写页面的时候,实际上可能需要将一些小片段的节点分解成小组件。内部复用,这些小组件其实可以通过编写一个简单的功能组件来完成。如果你现在没有这个习惯,可能是由于SFC的限制,你习惯把所有的东西都写到一个文件中。
例如,这里我们封装了输入组件。我们希望将Password组件和Textarea组件同时导出,方便用户根据自己的实际需求使用。这两个组件本身内部使用了Input组件,但只自定义了一些props。在JSX那里很方便。基本上只要写一个简单的功能组件就可以了。只需通过接口声明 props 即可。但如果是写在模板里的话,可以分成三个文件,也许加上输入文件index.js
来导出这三个组件,钓鱼时间就更短了。
严重依赖编译时检查
模板引用了script
中未声明的a,vscode插件可以帮助检查但仍然可以运行。
如果用TS写,这里引用了未声明的变量c,TS可以防止代码在编译阶段直接运行。目前,模板仍然直接编译为 JS,因此尚无法对 template
进行编译时类型检查。
拥有完整的JS编程能力
因为JSX的本质是JavaScript,所以拥有完整的JavaScript编程能力。例如,我们需要使用一段逻辑来反转一组 DOM 节点。如果用模板来写,估计要写两段代码。
虽然这个例子可能并不常见,但必须承认,在某些场景下,JSX还是比模板写得舒服。
<A>
{{
default: () => (
// children
)
}}
</A>
图案组件
在模板中,由于历史原因,通用组件目前不支持它,但这并不意味着它将来不起作用。如果必须使用泛型类型,可以先使用函数组件包装它,但要注意不要声明 FunctionalComponent
类型。这里我们在文件 .tsx
中声明组件 Foo
,并且 Props 是泛型类型。声明完成后,返回模板,可以看到刚才定义的通用组件已经生效了。 TS IDE SFC 支持可以使用volar。 Volar 还支持通用组件,并且在使用上与 TSX 几乎相同。
使用JSX时的注意点
搬运道具
道具的处理合并在模板中。为了满足不同用户的需求,开了一个可以弥补的缺口。
搬运槽
slot是一个内容分发的API,外文称为Slot,是的最后一个参数。它适合在结果复杂并且文件夹内容可以重复使用的情况下使用。简而言之,可以在组件中保留空间,并且可以从父级传递内容。在JSX中,父组件通过属性将VNode传递给子组件就完成了。
但是在模板中传递属性时,VNode不能写在template
中,所以Vue中出现了槽的概念,槽仅在组件的子级中可用。
让我们看看 Vue 如何处理插槽:
Vue 的槽要求最好是对运行时性能有很大帮助的功能。因此组件A的子节点将被编译为{ default: () => [123] }
。对应JSX,按照正常用户的心理模型,只有一个孩子的时候,写成{ default: () => [123] }
不太现实。正常的招生方式是直接插孩子。但必须在编译时处理成函数,否则开发时会报警告,这对于开发者来说是非常不友好的体验。其实对于编译器来说很简单,只要将函数层包裹在子节点的VNode中就可以了。
在多个插槽的情况下,它比单个场景要复杂一些。对于非默认插槽,不可能通过 props 传递它们。我们只需找到一种方法以类似于“指令”的方式传递它们即可。因此,命令v-slots
最初是为了处理槽而设计的。但v-slots
对于某些开发人员来说可能并不直观。更直观的方法是obejct slots
:
简单说一下两个概念:编译和运行时。编译是将我们的代码转换为 JavaScript 引擎可以理解的代码,并在运行时 JavaScript 引擎开始执行您的代码。就像我们招聘中的简历筛选和面试一样,简历筛选可以匹配构建和面试来运行。仅凭简历你无法判断他是一个什么样的候选人。现在回到问题,直接将children写成内联对象很容易,但是如果是变量的话,在编译时编译器无法知道传递的是什么,是槽还是实际上VNode不能编译时看到的。如果它在一个文件中,编译器仍然可以评估它,但如果它是从另一文件导入的,则无法评估它。 Babel 将每个文件作为“闭环”处理。所以此时需要添加运行时判断:
即使解决了判断是否是槽的问题,但是给每个变量添加运行时判断,对编译后的产品体积还是会有一定影响的。 jsx-next #255
为了保持编译后产品的体积和直观语义之间的平衡,让开发者选择是否需要上述功能,并提供一个开关enableObjectSlots
。
模板与JSX业绩对比
刚才说了一些用JSX可能会更好的场景。这里简单对比一下JSX和模板实现相同功能的性能差异。左右两个示例中有 20,000 个节点。在奇数节点之间,class
是动态的,在偶数节点中,textContent
是动态的。点击随机。在这个例子中,使用模板编写的代码比使用JSX编写的代码快了十几毫秒。在现实场景中,组件的分层嵌套比此处所示的演示要复杂得多,此时可能更好地体现模板的好处。
在传统的VDOM树中,我们无法在运行时获取用于优化的信息。在Vue 3中,模板的静态信息被充分利用,最终会反映在VDOM树中。例如,在 diff 期间您可以知道哪些节点是动态的以及哪些节点属性是动态的。我们可以利用这些信息来表明哪些属性在创建VNode时是动态的(有针对性的更新),这就是传说中的PatchFlags。除了PatchFlags
VDOM 之外,Vue 3 还在运行时做了一些缓存,比如子缓存。
首先让我解释一下PatchFlags
的工作原理。其实只是一个数字,但在运行时却有不同的含义:
-
数字 2 (PatchFlags.CLASS):表示该类是动态的
-
数字 4 (PatchFlags.STYLE):表示样式是动态的
可能有的同学不明白这样表达的好处CLASS = 1 << 1
,其实是用二进制表达的,如上面的代码:
TEXT = 0000000001
CLASS = 0000000010
STYLE = 0000000100
例如,如果节点的类和样式都是动态的,则用PatchFlags.CLASS | PatchFlags.STYLE
标记,即可得到0000000011
。如果你想判断它的TEXT是否是动态的,只需要FLAG & TEXT > 0
。
好像如果把props
属性标记出来,JSX好像也可以标记VDOM?
让我们看一个稍微复杂一点的场景。我们看到textarea
依赖于attrs,所以编译后对应的PatchFlag应该是
_createVNode("textarea", _mergeProps({
"id": "textarea"
}, attrs), null, 16);
提取这段代码单独运行是没有问题的,但是因为外层textarea
包含了一些组件,attrs是单独定义的变量,所以没有响应。忽略 attrs 变量并将此代码视为模板。当模板编译时,A的后代实际上在编译过程中进行了一层缓存。无需在每次重新渲染时创建子 VNODE。同时,为儿童创造了一个封闭空间。如果在编译此代码并检查静态标记时缓存子级,则 attrs 将始终获得第一次绘制的值。
<A>
{{
default: () => (
// children
)
}}
</A>
单击按钮后,视图的更新不会开始。此时,只能放弃组件A的优化,子级不会被缓存。因此,一旦将无响应的变量传递给子节点,其父节点的所有子节点都会放弃缓存,因此每次新渲染时都会重新创建它,优化不是很明显。然而,上述写作风格在JSX身上却很常见。
除了PatchFlags
之外,Vue 还有一个名为 的概念来处理各种儿童情况。在上述情况下,子级必须标记为 DYNAMIC
才能离开子级缓存。所以如果你用JSX写 Vue,你基本上无法享受优化 Vue 3 创建的模板。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。