Vue3 发布(2020 年 09 月 18 日)至今其实有一段时间了,虽然之前关注过许多关于 Vue3 的介绍,心理也一直很痒痒,寻思有一天一定要亲自试试,没想到一拖就拖了 1 年多 🤦
不知道大伙是不是跟我一样,网上的简短介绍看了许多,大致的改动也清楚,但是从来没有亲自试过…
我上家公司是在使用 Vue2 技术栈,所以对上一代 Vue 的基础理论和运行机制是比较了解的。由于公司内部技术栈是 React,好不容易找到了一个项目 —— 油猴脚本,抱着“我看看有啥不一样”、“学习为主,不着急完工”的心态,使用 Vue3 完成了该项目的开发(其实是先用 React 写了一版,然后又拿 Vue3 写了一版 😂)
也可能是现在对 React 也有了一定的理解和经验,再回头看 Vue 确实重新引发我了我对这 2 个框架的更深层次的思考和理解。
虚拟 DOM
Q: 想象一下,你现在还记得没有框架的时候,我们应该如何完成一个表格 dataSource 的渲染?
首先,大家都知道 2 个框架都是有虚拟 DOM 的,在 React 中,几乎没有人没见过 vDom 的对象,因为一个 JSX 返回的对象就是它。但是在 Vue 中,这一概念更像是被藏起来的背后运行机制,如果你不想特意的去看看 Vue 中的 vNode 对象,你几乎可以一直不用见到它,哪怕有一天 Vue 把虚拟 DOM 机制给去掉了。
这对于再回到 Vue 的开发的我来说,还是有一定冲击的,我一直在试图寻找虚拟 DOM。但这不得不让我思考另外一个问题:
Q: 通用八股文,我们为什么需要虚拟 DOM?
当然,Vue 中雪藏虚拟 DOM 的操作,也着实可能会带来不少问题,比如:
- 组件设计使用 slot,代替 React 的
React.ReactNode
类型传参,但如果组件开发者真就没注意设计 slot,而是将Props
定义成了一个string
传参来处理,那么在模板语法中,你将没有办法自定义他的渲染结构。这其实是很多组件开发者容易忽略的,比如:label
、content
、message
这种字段,极容易设计成一个string
,导致组件使用者想要自定义这部分内容的时候,根本没有办法啊 😭! - 全局函数式调用弹窗,无法自定义渲染内容等;当然 Vue 也给出了解法,就是使用 JSX。所以你会在一些库中看到类似这样的 Vue 代码 😂:
import { h } from "vue";
this.$message.info({
content: This is an info message!,
icon: ,
});
性能
Vue3 说它的性能很强,我实践下来看,是真的好强!🤩
我先不说那些常规的什么 diff
算法,内存优化之类的常规优化手段。Vue 中提供一个 Playground ,让你可以清晰简短的体验 Vue 特性。
编译时优化
在这一段实例中,你可以清晰看到,Vue 分析出了我在模板中的纯静态的部分,这部分作为常量直接被放在外面,这样在执行 re-render 的过程中将不需要再次创建这层虚拟 DOM ,这在 Diff 过程中也会节省相当大的递归运算。
更新粒度
此外,Vue 利用的依赖追踪,可以轻松的通知到所有使用其状态完成运算的节点,这将意味着它可以轻松完成仅对需要变化的组件进行 re-render 。
虽然现状来看 Vue3 还是按照组件级别的粒度进行组件的更新,但根据 Solid、Svelte 等框架实践的成功来看,Vue3 确实可以根据依靠这一特性继续细化到真正按需 re-render。
到这个时候其实也就不需要 vDom 的辅助了,vuejs/petite-vue: 6kb subset of Vue optimized for progressive enhancement 其实就是对此的尝试。
但即使是现在,Vue 借助 Proxy 可以天然的完成类似 GlobalStore
的全局状态,也可以更加从容的完成全局状态依赖追踪,从而更加精确判断更新范围。而 React 的 Context 相对这一点来说,不是说做不到,是需要开发者有意识的去做。
Vue 很重要的一点就是,把很多 React 需要特意做才能处理好的性能问题,直接黑盒化了。你即使了解的不是很深入,写的东西一点都不讲究,它的性能也能保持在一个不错的程度。
React Hooks != Vue Composition
Q: React Hooks 是如何运行的?为什么 setState 后会重新运行当前函数?它是怎么做到的?
虽然乍一眼看上去,Vue3 最大的变化不就是也走上了 FP 的道路么。这不就是抄了人家 React Hooks 么。
我一开始也这么想来着 🤦。但是当真正在使用的过程中,注意到 setup
这个 API 命名的时候,我意识到了事情的不简单。
React 确实在 FP 这个领域又一次做到了开创式的壮举,但是开创也意味着从第一版 API 到现在需要一直保持可用,也意味着必须负重前行(背着历史包袱),但这样的设计或许并不是最好的。
React Hooks 实际上是在创建 VDom 的过程中定义状态。它混合了这个状态定义和创建虚拟 DOM 的过程,这就导致了一些问题:
- React Hooks 的顺序必须保持一致。任何时候,无论那种状态,你需要保证 React 的 Hooks 定义需要在首次运行过程中全部命中,不然在 React 中则会出现异常。
- 在 React 中,一切定义状态默认值和 deps 的地方,都是对内存的浪费,如果你存在类似
useState(new Class())
这样的定义,咱们知道new Class()
只会在第一次生效,但实际上它却是实实在在每次 render 都会执行的,这很浪费,也很奇怪不是吗?
Vue 在这里的定义就相对比较分离,借助于 Proxy 的依赖追踪,Vue 其实并不担心对于后续状态的使用的追踪。
这意味着它可以直接在初始化的函数中就定义好所有的状态和状态关系,也就是 setup
。
所以在 Vue 中,每个组件的 setup
其实只运行一次,在 setup
中定义和返回的值都将作为可响应对象被模板中消费。
所以非 jsx 写法的 Vue 组件,实际上状态的定义和 VDom 生成是分离的。
有了以上这些特性的加持,实际上 Vue 已经可以做到即使开发者不怎么注重性能和封装的情况下,也能做出性能不错的产品。
一个 React 的 Hooks 组件,如果有 2000 多行,或许它会直接让你的页面变卡顿来表示你需要更加好的封装来完成更小粒度的组件状态管理。
但一个 2000 多行的 Vue 组件或许不会,它表面依旧平静顺滑的像一个精心雕琢过的艺术品,当然如果你不考虑后续维护成本的话 😂。
所以,我不得不说的一点是,或许、相对的、大概率 …(以及 N 多防喷条件)的情况下,Vue 更加容易写出来屎代码。因为它本身太强了 🤦
并且,Vue 的性能还没走到尽头,看得见的,它还有去掉 vDom 的路子可以走。而 React 嘛… 或许还可以创新出一条路,毕竟这不是它第一次干这种事了。
数据流(双向绑定)
在 React 中,我们设计表单组件的时候,往往会使用 value
、defaultValue
、onChange
这些属性来控制一个组件值的受控与非受控。并且在 React 中,对于所有需要组件双向绑定的行为,都需要这样去做一层处理,例如:visible
、defaultVisible
、onVisibleChange
。
在习惯了这样的设计后,其实我也没觉得有什么,好像 Form 组件本身确实就是有必要这样去做的,否则怎么来同步它的值状态呢。
这不得不让我在思考一个问题:
Q: 我们为什么需要受控或不受控?它本质在解决问题?
在 React 中,value 的下传就表示 state 由父组件管理,必须通过 onChange 通知 value 的变化,然后进行 setState,从而实现数据与视图的同步。
但在 Vue 中,使用 v-model
指令处理数据的双向绑定,详细参考:组件 v-model | Vue.js
本质上还是在进行一个 dom 和状态的同步绑定,但为什么在 Vue 的各类组件库设计中,就看不到(或者很少看到)类似 defaultValue
、受控非受控 这类的概念呢?
在重写写回 Vue 的这段时间里,又让我重新思考这个问题:我们为什么需要受控或不受控?它本质在解决问题?
受控、非受控,或许这本身就是不应该存在的概念。
从原生的 input
来看这件事情,input
的数据本身没有意义,它是结合 form
来做数据填写存在的。如果你一定要进行单独使用,只需要通过 dom 操作,获取 input
本身的 value 即可。
所以对于输入型组件数据的状态同步这件事情,或许应该弱化 “事件” 的概念,通过一个语法糖指令 成对的 消除这种状态同步所出现的差异问题。
不下传 v-model
则表示,这个组件不需要状态同步;下传 v-model
则表示已经同步了状态同步,只要所有的组件 follow 这种规则和设计,那就不存在非受控的概念,因为他们的出现一直都是成对的。
结
不得不说的是,Vue 的这次故地重游,让我再次理解了以前许多甚至都不觉得是问题的问题。
所谓成长,就是通过经历,了解问题的本质和细节。这样才能在下次遇到类似问题时,能够做出客观的评估。
技术层次不穷,没有哪个是 绝对好或绝对不好,站队对喷的行为不是作者愿意看到的。分享、创造 不是为了获得认同,不认同划走便是。
“施主,莫要 着相 了”