尤大大从下面的三个前端领域的不同层次来展开了介绍:
开发范式&底层框架(注:大家比较熟悉的Vue、React这些框架层面)
工具链(注:像webpack这样的构建工具)
上层框架(注:例如Next.js、Nuxt.js)
正式分享之前,尤大大提出声明:“本分享只代表讲着个人观点,因为自己是框架和构建工具的作者,肯定会包含利益相关和个人的偏见,但是分享中会尽可能做客观的看法,大家多多多包涵”,下面就让我们饱享这顿“美味”吧!
下面的内容是根据尤大大的分享进行了一定的抽离和少许的个人总结,如果内容出现歧义可以在评论区留言!
过去几年中影响最大的开发范式层面的变化肯定就是我们的 React Hooks
随着他的推出可以说是启发了很多组件逻辑表达和逻辑复用的新范式,在 React
生态中彻底取代了 Class Components
,包括现在其实很少能够在 React
中看到 Class Components
了,
不仅如此,其实在其他的框架中 React Hooks
也产生了很大的影响,比如说我们 Vue
推出的 Vue Composition API
组合式API,还包括受到 React Hooks
的启发的 Svelte3
,更有 SolidJS
他是语法上相似于 React Hooks
实现上更相似于 Vue Composition API
。
随着 React Hooks 的推广和开发者对其的广泛使用,他开发中的一些体验问题也逐渐被正视,这里不可回避的一些体验问题的根本原因有以下几点:
Hooks
执行原理和原生JS的心智模型的差异:因为 React Hooks
是通过把组件的代码每一次更新都进行重复调用来模拟一些行为,从而导致反直觉的一些限制;
不可以条件式的调用 React force
;
Stale Closure
的心智负担:如果你不传正确的依赖数组,那么就会产生过期闭包;
必须手动声明 use Effect
依赖;
如何‘正确’使用 use Effect
是个复杂的问题;
需要 useMemo/useCallback
等手动优化,否则的话就会不知不觉的导致一些性能问题;
尤大大表示作为竞争框架的作者,对 React Hooks 框架的看法可能相对更直接一些,但这些也并非尤大大一个人的看法,而是近年来 React 社区和 React 团队也已经意识到的问题,当然 React 团队针对这些问题也在做改善的努力,据代表性的改善从下三个方面:
在上面的这些改进之前,其实很多 React 的社区成员也包括一些本身就不适用 React 的用户来说,虽然 React Hooks 产生了重大的影响但是大家也意识到了他的一些问题,反而是一些跟 React Hooks 相似的一些逻辑组合能力,另一方面基于依赖追踪的范式开始重新得到了重视;比如在 React 内部的 Recoil ,当然在社区之外就有更多了比如:
我们可以看一下就基于依赖追踪的范式而言上面三个方案的代码:
SolidJS
//状态
const [count,setCount] = createSignal(0)
//副作用
createEffect(() => console.log(`${count()`}
//状态更新
setCount(count() + 1)
能够看出其实 SolidJS 和 React Hooks 非常相似
副作用中的 createEffect 跟 React 中的 use Effect 其实是类似的,但是 createEffect 并不需要去声明依赖,在调用 count 函数的时候其实帮你收集了依赖;
状态更新的时候我们也并不需要用到 useCallback 这种额外的方式去创造函数来去传递给我们的事件侦听器;这些都是非常符合直觉的;
Vue Composition API
//状态
const count = ref(0)
//副作用
watchEffect(() => console.log(count.value))
//状态更新
count.value++
其实 Vue
中使用的 Composition API
跟 SolidJS
本质上的内部实现几乎是一样的,只不过 SolidJS
看起来更像是 React
,而 Vue
是通过一个 ref 对象,对象上的 value 机可以读也可以写,在读和写之中就会自动的追踪和更新依赖。
Ember Starbeam
//状态
const count = Cell(0)
//副作用
DEBUG_RENDERER.render({render: () => console.log(count.current)})
//状态更新
count.set(prev => prev + 1)
,Ember Starbeam
中的这个 Cell
其实就和 Vue
中的 ref
api 几乎是一样的,暴露出 count
为当前的值和 set
方法来进行状态的更新
上面提到的三种基于依赖追踪的范式他们的共同点有什么呢?
同时以依赖追踪为一等功名概念的框架中,本身组件的设计肯定也是跟依赖追踪有紧密的结合,所以组件的更新渲染也会有自动的依赖追踪,也就是说组件的更新会更精确,而不再依赖于一个状态从父组件到子组件一层层传递下去,而是每一个即使是深层嵌套的组件也可以自发的更新,整体上的性能会更好。
在 react
生态中的 Recoil
这样的方案,虽然也提供了依赖自动的依赖追踪和一定程度的逐渐的更新优化,但是因为他们仍然是需要在 React Hooks
的这个大的体系中使用的,所以在很多其他的方面依然会受制于 hooks
的问题,那么 Hooks
本身在这些方案之外,还是会存在过期闭包等等 user fact 这些问题。
React Hooks
确实是启发了一个新范式的时代,但是慢慢的我们也发现他自己自身存在的一些问题,当然 React
团队正在试图解决这些问题,同时在 React
体系之外,开始有一些其他的具有同等的逻辑组合能力,但同时避免了 React Hooks
这些问题的这些方案存在,也渐渐的收到了前端社区的重视。
不过即使是基于依赖追踪的方案,我们也可以进行一些基于编译时的这个优化,那这里首当其冲的就是 Svelte3
Svelte
//状态
let count = 0
//副作用
$:console.log(count)
//状态更新
count++
Svelte3
从一开始就是一个编译时优化方案,上面就是 Svelte
组件中的一个使用状态的代码,我们看到他跟他的状态就是这个 javaScript
的这个 let
这样声明一个变量,就是一个响应式的状态,那么你要更新这个状态就直接去操作这个变量就可以,
副作用是用一个神奇的编译式的魔法,也就是这个 $
,这个 $
的一个label,这其实是 javaScript
的一个label语法来声明, $
之后的这个语句会自动去追踪count
这个变量的变化,当count
变化的时候,这个语句就会自动重新执行,那么我们可以看到这个跟我们之前的这个几个代码范例,他所达成的目标其实是一致的,只是他使用编译的手段使代码变的更加简洁,但也正是因为简洁所以存在下面的限制:
Vue Reactivity Transform
也正是受到上方的限制的启发,Vue 在3.2的时候引入了一个实现性的功能 Vue Reactivity Transform
响应式转换 ,下面就是 Vue 转化后的一段代码:
//状态
let count = $ref(0)
//副作用
watchEffect(() => console.log(count))
//状态更新
count++
还是一个简单的变量声明,但是我们用一个 $ref 这样的一个函数,这个函数其实是一个编译时的一个宏的概念,这个函数并不是真实存在的,只是给编译一个提示,那编译器通过编译之后就会把它转化成我们之前看到的基于真实的 ref 的代码。
但是在使用时候,体验就变成了只是声明一个函数,然后使用这个变量和更新这个变量就跟使用一个普通 javaScript
变量没有区别。同时这个语法因为在声明的时候会显式的声明,说哪个变量是响应声,哪个变量不是响应式。
所以这个语法可以在嵌套的函数中使用,也可以在 TS/JS 文件中使用,他并不限制于 Vue 文件,所以这是一个更加朴实的编译响应式模型。
Solid -labels
//状态
let count = $signal(0)
//副作用
$effect(() => console.log(count))
//状态更新
count++
在 Solid
的生态中,其实也受启发于 Vue Reactivity Transform
,他的社区用户做的一个 Solid-label
,也是基于 Solid
的响应式方案,然后再做一层编译式的优化,那么可以看到跟 Reactivity Transform
能够达成的效果是非常相似的。
那最终的目的就是让大家可以用更简洁的代码去表达组件逻辑,同时又不放弃这个逻辑组合,像 React Hooks
那样进行自由的逻辑组合的这些能力啊。所以说这也是一个很有意思的探索方向。
优势: 和Svelte
相比,Vue的 Reactivity Transform
和 Solid \-labels
都属于统一模型,也就是他不受限于组件上下文,它可以在组建内使用,也可以在组建外使用,优势就是有利于长期的重构和复用,因为很多时候我们的大型项目中的逻辑复用都是在我们一个组件写着写着发现这个组件变得很臃肿,很大的时候我们才开始考虑要把逻辑开始重新组织抽取复用,那么由于 Svelte
的语法只能在组件内使用,就使得把逻辑挪到组件外成为一个代价相当大的一个行为,并不是一个简单把文把这个逻辑拷贝复制出去,而是需要进行一次彻底的重构,
因为组件外用的是完全一套不同的系统,但是像用 Reactivity Transform
和 Solid \-labels
这样的方案呢,我们就可以把组件内的这些逻辑原封不动的直接拷贝到组件外,然后把它包在一个函数里面,抽取就完成了,那么这样重构时的这个代价就非常小,也就更鼓励团队的这样的优化,对于长期的维护性更有帮助。
代价: 因为我们需要显示的去声明响应式的变量,所以它会有一定程度的底层实现的抽象泄露,也就是说,用户其实是需要先了解底层的响应式模型的实现,然后才能更好地理解这个语法糖是如何运作的,而不像 Svelte
组建中的这个语法,即使你完全不了解他底层如何运作的也可以,几乎可以零成本的上手,这就是一个长期的可维护性和一个初期的上手成本之间的一个平衡和取舍。
讲完了状态管理,我们在还可以聊一聊关于基于编译的运行时优化,编译的运行时优化又是三个主要的代表,如上图所示,那首先我们可以看一下不同的这个策略:
Svelte
的这个代码生成策略相对更更繁琐一些,而 Solid
是基于先生成一个基本的HTML字符串,然后在里面找到对应的 DOM
节点进行绑定,而 Svelte
是通过生成一这个命令式的一个一个节点,然后把节点拼接的这些 javaScript
代码,但这个策略就导致掉同等的这个组件源码之下 Svelte
的每个组件的编译输出会更臃肿,所以虽然大家感觉 Svelte
是以轻量出名的,
但其实我们会发现在相对大型的项目中,在项目中组建超过15个之后,Svelte
的整体的打包体积优势就已经几乎不存在了,那么当组建超过50个,甚至是达到100个的时候,所有的体积会越来越越来越臃肿。
而相对于而言,我们可以看到 Vue
和 Solid
的编译这个输出啊,整体的这个曲线就平缓很多,所以其实在越大型的项目中。反而是 Svelte
的体积优势反而是一个劣势,据我所知,Svelte
团队也有在想要优化这一方面的,可能会在下一个大版本中才能实现,那么我们也会拭目以待。
同时尤大大提出 Solid
的编译性能确实是非常的猛,其实在我们的 Vue 引入了很多编译时的优化以后我们的性能已经比 Svelte
好了,但是离 Solid
还是有一定的距离。
就上面提及到的编译时性能优化,其实我们的 Vue 在早期的时候也做了这方面的探索,如还在试验中的一个项目 Vue Vapor Mode
。
那同样的这个只有单文件组件输入,我们现在是通过把模板编译成虚拟DOM
的一个渲染函数来进行运行时的实现。但是因为模板是一个编译源,所以我们也可以选择在另一个模式下把它编译成不同的输出,也就是一个更类似于 Svelte
输出。
import { ref,effect } from 'vue'
import { template,on,setClass,setAttr,setText } from 'vue/vapor'
const t0 = template(`