前言
在了解了下 React, Vue 这些优秀框架的源码后,对这些框架做个大概的总结。
这里只讨论基础框架, 那些 UmiJs, NuxtJs 等这类暂不讨论。就基础框架而言,我个人知道的主流框架有 Angular, React, Vue, Solidjs, Svelte。
在国内用的最多的,应该就是 React, Vue 了。主要也是围绕这 2 个框架进行讨论。
框架对比介绍
能力有限只会对 React, Vue 框架进行讨论。并不全面,主要是一个个人向的记录,边学边加。(doge)
1. Template or Jsx?
首先个人感觉 Template 和 Jsx 其实是一家人,拿 Vue 来举例, 它的模板是将AST 转换成 VNode 的生成函数,内部是通过 语法解析(parse), 转换(transform), 生成代码(codegen) 几个步骤,而我们的 Jsx 也是利用 Babel Parse 基于 AST,在通过以上几个步骤来解析的。
但重点在于,Template 和 Jsx 在使用方面上的不同。
Jsx 的特点便是灵活,简单,容易上手,不需要记太多 API
// Jsx代表框架 -- React
export default () => {
const list = [1, 2]
// 下面这段就是用 jsx 创建的循环
return (
<>
{list.map((v) => (
<span key={v}>No.{v}</span>
))}
</>
)
}
而 模板Template,它有着自己的一套约束,虽然在灵活度上就显得不足,但模板对于框架开发者而言更加友好,使得开发者可以创建各种语法协助构建 DOM
<!-- Template代表框架 -- Vue -->
<template>
<span v-for="item in [1, 2]">No.{{ item }}</span>
</template>
2. 响应式逻辑
Vue :
const list = reactive([1, 2])
list.push(3) // 触发 list 的 set ,进而触发 trigger
const count = ref(0)
count.value = 3 // 触发 Ref类实例 里的 set value() 访问器,进而触发 trigger
Vue 大家比较熟知, Vue2.x 是通过 Object.defineProperty 给对象的每个属性绑定上一个 get/set 访问器。
Vue3.x 的 reactive 主要是通过 Proxy 进行代理, 而 ref 是通过 Class 访问器, 这里的 Class 指的是Ref 类实例内部创建了一个 value 的 get/set 访问器, 具体可以看我 《Vue 源码之【响应式】》 的文章。
React :
const [count, setCount] = useState(0)
setCount(1) // 触发
React 从 V16 开始就采用 Fiber 架构,且在 V16.8 开始可以使用 Hook 进行 函数式组件 的状态处理。
这里只简单介绍下大概流程: 用户调用 setState 去修改数据之后,会触发更新,从当前位置开始顺着 Fiber链 向上修改 lane 优先级。然后开始向下调和,找到触发更新的那个 Fiber 进行 rerender ,函数组件的 rerender 会重新调用 函数,使得重新渲染后值就改变。
Solid :
const [count, setCount] = createSignal(0)
setCount(count() + 1) // 触发局部更新
Solidjs 的响应式数据用法和 React Hooks 类似,但实现却和 React 不同,框架整体采用的思路跟 Vue 类似,不过不同的是,他内部是通过函数闭包的方式创建了 getter 和 setter。具体有想了解的小伙伴可以找找。我也还没弄透,简单写一个大概...
function getter() {
return this.value
}
const createSignal = (value) => {
const obj = {
value,
}
const setter = (newValue) => {
obj.value = newValue
}
return [getter.bind(obj), setter]
}
const [count, setCount] = createSignal(1)
3. 更新颗粒度
更新的颗粒度直接与性能挂钩,就目前来说,颗粒度可以划分为三级:应用级,组件级,节点级 颗粒度越细腻,理论上来说性能也就越好。
React 属于应用级框架,之所以性能一直被鞭尸,也是由于其每次 rerender 的压力非常大。从 Fiber 链表向下调和,再在目标节点处开始 render,所以经常会有 子组件渲染 是因为 父组件 被渲染了 这种尴尬的地方……
Vue 属于组件级框架,其渲染是跟组件挂钩的,当响应式数据被更新时,进而会触发到其组件的更新方法(componentUpdate),但是 Vue 不会随便去触发子组件渲染,而是会去用 shouldUpdateComponent 去对比组件前后属性,props 等是否有变化,如果没有变化,就不会再继续走了。
有意思的是, Vue1.x 也是节点级的应用,但后面由于 Object.defineProperty 绑定对象的开销特别大,容易导致内存过大,所以从 2.x 开始就放弃 节点级 采用 组件级的方式来优化。
Solid 和 Svelte 属于节点级框架,所谓节点级,即用户更新时,非响应式数据的部分都不动,只单独处理响应式数据那块逻辑。具体细节,还是需要读者去查询一下,篇幅太长,不做过多了解(doge,我也不是特别了解……)
4. 虚拟 DOM
这块其实和上面是连着的,虚拟 DOM 开发的初衷,就是为了减少 DOM 的操作次数。然后你会发现,Solid 和 Svelte 都是属于 无虚拟 DOM 的框架。这便是得力于二者的节点级更新粒度的优势,想象下:我都知道我要更新的是哪个节点了,我又何必去增加 JS 开销弄一个虚拟 DOM 去找这个节点……
组件级 和 应用级 都需要虚拟 DOM,便是由于 一个组件 或者 一个应用被重新更新,其内部非常多节点,他并不知道具体更新的是哪一个节点,所以需要去比较前后哪些节点发生了变化,然后用算法计算出如何操作节点更节省开销
5. Typescript 支持
React 写 Ts 写起来非常舒服,不管是组件泛型还是导入第三方类型,相当舒服。
Vue3 我前段时间尝试开发过 Ts + SFC 的组件,为了良好的类型,我去兼容第三方类型,但是 Vue3.2 及以下版本中,defineProps 的泛型类型参数仅限于字面量类型或对本文件接口的引用 导致类型写得特别蛋疼,不过在最近, Vue3.3 已经支持了。只能说我当时那会确实尴尬,还有就是 Vue3 的第三方框架的类型特别难以看懂……他有 2 套类型,一个动态运行时声明类型,还有一个就是 泛型声明 ,你如果做组件,想做好兼容,可能需要导出 2 套类型……
当然还是很期待 Vue3 和 Ts 越来越香~
6. 开发体验对比
React 开发优势
-
函数就是组件,可以随处导出导入组件,且在一个文件里,可以同时写多个组件。
-
万物皆props,哪怕是一个 React 组件 ,你也可以当props去传递。 -
因为组件就是函数,所以组件里调用自己,就是递归自己,非常简单。
React 开发槽点
-
useEffect依赖容易出现 BUG,应该有不少人遇到过类似的…… -
目前没有最合适的
KeepAlive的解决方法(网上有,我也自己写过,但是就怕遇到奇奇怪怪的 bug,且有些用display: none的伪缓存方案包括我自己写的那个,还是存在一些问题) -
当业务多了,结合其他第三方框架容易导致页面渲染 BUG。
-
想要获取子组件的属性不太容易,需要调用
forwardRef,这个forwardRef的类型又很奇怪,写起来很难受 -
由于
state并不是最新的,但是我们又需要给后端最新的值,导致很多时候,需要用到ref和state配合……但明明是同一个的状态,我举个场景大家看看能不能 get 到:
一个弹窗,显示后左边是 Tree,Tree 上有 Search 搜索框,右边是分页 Table,表格上也有 Search 搜索框
然后我右边输入搜索内容的时候,然后我在左边点击切换 Tree,我需要更新掉这个搜索框内容 和 分页
但是由于点击 Tree 的时候,要去调用 分页 接口,传入不同的 TreeId ,
但是由于 Search 和 分页 是在点击之后调用 setState 修改的,就导致……后端那边的还是之前的值,
当然也有解决方法: 用 ref 或者 在点击Tree的时候,给 分页接口 主动传递 空字符串 和 初始分页状态
有朋友问,为啥不写在 useEffect 中? 主要是不好控制,业务多了就不好控制……哎嗨
所以我现在主要是用 ref 和 update 来控制。把数据存到 ref 里,在通过主动调用 update 去触发更新。
Vue 开发优势
-
响应式非常灵活,是和组件完全解耦的,可以单独写在任何地方,你甚至可以把他拿来做全局状态管理器,这点
React就不行,hooks必须在组件内部…… -
KeepAlive缓存舒服,虽然有些内存上的问题,但是很多后台项目真的很需要这个…… -
数据更新很直观,没有压力,降低用户心智负担(对比 React 槽点 5)
Vue 开发槽点
-
Vue的语法糖特别多,一不留神可能就又出了新的 API -
下传的种类非常多
props,还有emits, attrs, slots在SFC里,每个都有独立的defineXxxx的 API -
且插槽真的坑爹,所以导致我想封装那种灵活度高的组件都特别不容易