1. 引言
Vue 3.0 的发布引起了轩然,让我们解读下它的 function api RFC 详细了解一下 Vue 团队是怎么想的吧!
首先官方回答了几个最受关注的问题:
Vue 3.0 是否有 break change,就像 Python 3 / Angular 2 一样?
不,100% 兼容 Vue 2.0,且暂未打算废弃任何 API(未来也不)。之前有草案试图这么做,但由于用户反馈太猛,被撤回了。
Vue 3.0 的设计盖棺定论了吗?
没有呀,这次精读的稿子就是 RFC(Request For Comments),翻译成中文就是 “意见征求稿”,还在征求大家意见中哦。
这 RFC 咋这么复杂?
RFC 是写给贡献者/维护者的,要考虑许多边界情况与细节,所以当然会复杂很多喽!当然 Vue 本身使用起来还是很简单的。
Vue 本身 Mutable + Template 就注定了是个用起来简单(约定 + 自然),实现起来复杂(解析 + 双绑)的框架。
这次改动很像在模仿 React,为啥不直接用 React?
首先 Template 机制还是没变,其次模仿的是 Hooks 而不是 React 全部,如果你不喜欢这个改动,那你更不会喜欢用 React。
PS: 问这个问题的人,一定没有同时理解 React 与 Vue,其实这两个框架到现在差别蛮大的,后面精读会详细说明。
下面正式进入 Vue 3.0 Function API 的介绍。
2. 概述
Vue 函数式基本 Demo:
<template>
<div>
<span>count is {{ count }}</span>
<span>plusOne is {{ plusOne }}</span>
<button @click="increment">count++</button>
</div>
</template>
<script>
import { value, computed, watch, onMounted } from 'vue'
export default {
setup() {
// reactive state
const count = value(0)
// computed state
const plusOne = computed(() => count.value + 1)
// method
const increment = () => { count.value++ }
// watch
watch(() => count.value * 2, val => {
console.log(`count * 2 is ${val}`)
})
// lifecycle
onMounted(() => {
console.log(`mounted`)
})
// expose bindings on render context
return {
count,
plusOne,
increment
}
}
}
</script>
函数式风格的入口是 函数,采用了函数式风格后可以享受如下好处:类型自动推导、减少打包体积。setup
setup
函数返回值就是注入到页面模版的变量。我们也可以返回一个函数,通过使用 这个 API 产生属性并修改:value
import { value } from 'vue'
const MyComponent = {
setup(props) {
const msg = value('hello')
const appendName = () => {
msg.value = `hello ${props.name}`
}
return {
msg,
appendName
}
},
template: `<div @click="appendName">{{ msg }}</div>`
}
要注意的是, 返回的是一个对象,通过 才能访问到其真实值。value()
.value
为何 返回的是 Wrappers 而非具体值呢?原因是 Vue 采用双向绑定,只有对象形式访问值才能保证访问到的是最终值,这一点类似 React 的 API 的 规则。value()
useRef()
.current
那既然所有 返回的值都是 Wrapper,那直接给模版使用时要不要调用 呢?答案是否定的,直接使用即可,模版会自动 Unwrapping
:value()
.value
const MyComponent = {
setup() {
return {
count: value(0)
}
},
template: `<button @click="count++">{{ count }}</button>`
}
接下来是 Hooks,下面是一个使用 Hooks 实现获得鼠标实时位置的例子:
function useMouse() {
const x = value(0)
const y = value(0)
const update = e => {
x.value = e.pageX
y.value = e.pageY
}
onMounted(() => {
window.addEventListener('mousemove', update)
})
onUnmounted(() => {
window.removeEventListener('mousemove', update)
})
return { x, y }
}
// in consuming component
const Component = {
setup() {
const { x, y } = useMouse()
const { z } = useOtherLogic()
return { x, y, z }
},
template: `<div>{{ x }} {{ y }} {{ z }}</div>`
}
可以看到, 将所有与 “处理鼠标位置” 相关的逻辑都封装了进去,乍一看与 React Hooks 很像,但是有两个区别:useMouse
useMouse
函数内改变 、 后,不会重新触发 执行。x
y
setup
x
y
拿到的都是 Wrapper 而不是原始值,且这个值会动态变化。
另一个重要 API 就是 watch
,它的作用类似 React Hooks 的 useEffect,但实现原理和调用时机其实完全不一样。
watch
的目的是监听某些变量变化后执行逻辑,比如当 变化后重新取数:id
const MyComponent = {
props: {
id: Number
},
setup(props) {
const data = value(null)
watch(() => props.id, async (id) => {
data.value = await fetchData(id)
})
}
}
之所以要 ,因为在 Vue 中, 函数仅执行一次,所以不像 React Function Component,每次组件 变化都会重新执行,因此无论是在变量、 变化时如果想做一些事情,都需要包裹在 中。watch
setup
props
props
watch
后面还有 、生命周期函数、依赖注入,都是一些语法定义,感兴趣可以继续阅读原文,笔者就不赘述了。unwatching
3. 精读
对于 Vue 3.0 的 Function API + Hooks 与 React Function Component + Hooks,笔者做一些对比。
Vue 与 React 逻辑结构
React Function Component 与 Hooks,虽然在实现原理上,与 Vue3.0 存在 Immutable 与 Mutable、JSX 与 Template 的区别,但逻辑理解上有着相通之处。
const MyComponent = {
setup(props) {
const x = value(0)
const setXRandom = () => {
x.value = Math.random()
}
return { x, setXRandom }
},
template: `
<button @onClick="setXRandom"/>{{x}}</button>
`
}
虽然在 Vue 中, 函数仅执行一次,看上去与 React 函数完全不一样(React 函数每次都执行),但其实 Vue 将渲染层(Template)与数据层(setup)分开了,而 React 合在了一起。setup
我们可以利用 React Hooks 将数据层与渲染层完全隔离:
// 类似 vue 的 setup 函数
function useMyComponentSetup(props) {
const [x, setX] = useState(0)
const setXRandom = useCallback(() => {
setX(Math.random())
}, [setX])
return { x, setXRandom }
}
// 类似 vue 的 template 函数
function MyComponent(props: { name: String }) {
const { x, setXRandom } = useMyComponentSetup(props)
return (
<button onClick={setXRandom}>{x}</button>
)
}
这源于 JSX 与 Template 的根本区别。JSX 使模版与 JS 可以写在一起,因此数据层与渲染层可以耦合在一起写(也可以拆分),但 Vue 采取的 Template 思路使数据层强制分离了,这也使代码分层更清晰了。
而实际上 Vue3.0 的 函数也是可选的,再配合其支持的 TSX 功能,与 React 真的只有 Mutable 的区别了:setup
// 这是个 Vue 组件
const MyComponent = createComponent((props: { msg: string }) => {
return () => h('div', props.msg)
})
我们很难评价 Template 与 JSX 的好坏,但为了更透彻的理解 Vue 与 React,需要抛开 JSX&Template,Mutable&Immutable 去看,其实去掉这两个框架无关的技术选型,React@16 与 Vue@3 已经非常像了。
Vue3.0 的精髓是学习了 React Hooks 概念,因此正好可以用 Hooks 在 React 中模拟 Vue 的 setup 函数。
关于这两套技术选型,已经是相对完美的组合,不建议在 JSX 中再实现类似 Mutable + JSX 的花样来(因为喜欢 Mutable 可以用 Vue 呀):
- Vue:Mutable + Template
- React:Immutable + JSX
真正影响编码习惯的就是 Mutable 与 Immutable,使用 Vue 就坚定使用 Mutable,使用 React 就坚定使用 Immutable,这样能最大程度发挥两套框架的价值。
Vue Hooks 与 React Hooks 的差异
先看 React Hooks 的简单语法:
const [ count, setCount ] = useState(0)
const setToOne = () => setCount(1)
Vue Hooks 的简单语法:
const count = value(0)
const setToOne = () => count.value = 1
之所以 React 返回的 是一个数字,是因为 Immutable 规则,而 Vue 返回的 是个对象,拥有 属性,也是因为 Vue Mutable 规则导致,这使得 Vue 定义的所有变量都类似 React 中 定义变量,因此不存 React 的特性。count
count
count.value
useRef
capture value
关于 capture value 更多信息,可以阅读 精读《Function VS Class 组件》 Capute Value 介绍
另外,对于 Hooks 的值变更机制也不同,我们看 Vue 的代码:
const Component = {
setup() {
const { x, y } = useMouse()
const { z } = useOtherLogic()
return { x, y, z }
},
template: `<div>{{ x }} {{ y }} {{ z }}</div>`
}
由于 函数仅执行一次,怎么做到当 导致 、 值变化时,可以在 中拿到最新的值?setup
useMouse
x
y
setup
在 React 中, 如果修改了 的值,那么使用 的函数就会被重新执行,以此拿到最新的 ,而在 Vue 中,将 Hooks 与 Mutable 深度结合,通过包装 ,使得当 变更时,引用保持不变,仅值发生了变化。所以 Vue 利用 Proxy 监听机制,可以做到 函数不重新执行,但 Template 重新渲染的效果。useMouse
x
useMouse
x
x.value
x
setup
这就是 Mutable 的好处,Vue Hooks 中,不需要 等机制,仅需一个 函数,直观的 Mutable 修改,就可以实现 React 中一套 Immutable 性能优化后的效果,这个是 Mutable 的魅力所在。useMemo
useCallback
useRef
value
Vue Hooks 的优势
笔者对 RFC 中对 Vue、React Hooks 的对比做一个延展解释:
首先最大的不同: 仅执行一遍,而 React Function Component 每次渲染都会执行。setup
Vue 的代码使用更符合 JS 直觉。
这句话直截了当戳中了 JS 软肋,JS 并非是针对 Immutable 设计的语言,所以 Mutable 写法非常自然,而 Immutable 的写法就比较别扭。
当 Hooks 要更新值时,Vue 只要用等于号赋值即可,而 React Hooks 需要调用赋值函数,当对象类型复杂时,还需借助第三方库才能保证进行了正确的 Immutable 更新。
对 Hooks 使用顺序无要求,而且可以放在条件语句里。
对 React Hooks 而言,调用必须放在最前面,而且不能被包含在条件语句里,这是因为 React Hooks 采用下标方式寻找状态,一旦位置不对或者 Hooks 放在了条件中,就无法正确找到对应位置的值。
而 Vue Function API 中的 Hooks 可以放在任意位置、任意命名、被条件语句任意包裹的,因为其并不会触发 的更新,只在需要的时候更新自己的引用值即可,而 Template 的重渲染则完全继承 Vue 2.0 的依赖收集机制,它不管值来自哪里,只要用到的值变了,就可以重新渲染了。setup
不会再每次渲染重复调用,减少 GC 压力。
这确实是 React Hooks 的一个问题,所有 Hooks 都在渲染闭包中执行,每次重渲染都有一定性能压力,而且频繁的渲染会带来许多闭包,虽然可以依赖 GC 机制回收,但会给 GC 带来不小的压力。
而 Vue Hooks 只有一个引用,所以存储的内容就非常精简,也就是占用内存小,而且当值变化时,也不会重新触发 的执行,所以确实不会造成 GC 压力。setup
必须要总包裹 useCallback
函数确保不让子元素频繁重渲染。
React Hooks 有一个问题,就是完全依赖 Immutable 属性。而在 Function Component 内部创建函数时,每次都会创建一个全新的对象,这个对象如果传给子组件,必然导致子组件无法做性能优化。 因此 React 采取了 作为优化方案:useCallback
const fn = useCallback(() => /* .. */, [])
只有当第二个依赖参数变化时才返回新引用。但第二个依赖参数需要 lint 工具确保依赖总是正确的(关于为何要对依赖诚实,感兴趣可以移步 精读《Function Component 入门》 - 永远对依赖诚实)。
回到 Vue 3.0,由于 仅执行一次,因此函数本身只会创建一次,不存在多实例问题,不需要 的概念,更不需要使用 lint 插件 保证依赖书写正确,这对开发者是实实在在的友好。setup
useCallback
不需要使用 useEffect useMemo
等进行性能优化,所有性能优化都是自动的。
这也是实在话,毕竟 Mutable + 依赖自动收集就可以做到最小粒度的精确更新,根本不会触发不必要的 Rerender,因此 这个概念也不需要了。useMemo
而 也需要传递第二个参数 “依赖项”,在 Vue 中根本不需要传递 “依赖项”,所以也不会存在用户不小心传错的问题,更不需要像 React 写一个 lint 插件保证依赖的正确性。(这也是笔者想对 React Hooks 吐槽的点,React 团队如何保障每个人都安装了 lint?就算装了 lint,如果 IDE 有 BUG,导致没有生效,随时可能写出依赖不正确的 “危险代码”,造成比如死循环等严重后果)useEffect
4. 总结
通过对比 Vue Hooks 与 React Hooks 可以发现,Vue 3.0 将 Mutable 特性完美与 Hooks 结合,规避了一些 React Hooks 的硬伤。所以我们可以说 Vue 借鉴了 React Hooks 的思想,但创造出来的确实一个更精美的艺术品。
但 React Hooks 遵循的 Immutable 也有好的一面,就是每次渲染中状态被稳定的固化下来了,不用担心状态突然变更带来的影响(其实反而要注意状态用不变更带来的影响),对于数据记录、程序运行的稳定性都有较高的可预期性。
最后,对于喜欢 Mutable 的开发者,Vue 3.0 是你的最佳选择,基于 React + Mutable 搞的一些小轮子做到顶级可能还不如 Vue 3.0。对于 React 开发者来说,坚持你们的 Immutable 信仰吧,Vue 3.0 已经将 Mutable 发挥到极致,只有将 React Immutable 特性发挥到极致才能发挥 React 的最大价值。