Skip to content

Commit

Permalink
109
Browse files Browse the repository at this point in the history
  • Loading branch information
ascoders committed Jul 1, 2019
1 parent b6487f8 commit 30bdcf1
Showing 1 changed file with 340 additions and 0 deletions.
340 changes: 340 additions & 0 deletions 109.精读《Vue3.0 Function API》.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,340 @@
# 1. 引言

Vue 3.0 的发布引起了轩然大波,让我们解读下它的 [function api RFC](https://github.com/vuejs/rfcs/blob/function-apis/active-rfcs/0000-function-api.md#comparison-with-react-hooks) 详细了解一下 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:

```vue
<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` 函数返回值就是注入到页面模版的变量。我们也可以返回一个函数,通过使用 `value` 这个 API 产生属性并修改:

```jsx
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` 才能访问到其真实值。

为何 `value()` 返回的是 Wrappers 而非具体值呢?原因是 Vue 采用双向绑定,只有对象形式访问值才能保证访问到的是最终值,这一点类似 React 的 `useRef()` API 的 `.current` 规则。

那既然所有 `value()` 返回的值都是 Wrapper,那直接给模版使用时要不要调用 `.value` 呢?**答案是否定的,直接使用即可,模版会自动 `Unwrapping`:**

```jsx
const MyComponent = {
setup() {
return {
count: value(0)
}
},
template: `<button @click="count++">{{ count }}</button>`
}
```

接下来是 **Hooks**,下面是一个使用 Hooks 实现获得鼠标实时位置的例子:

```jsx
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>`
}
```

可以看到,`useMouse` 将所有与 “处理鼠标位置” 相关的逻辑都封装了进去,乍一看与 React Hooks 很像,但是有两个区别:

1. `useMouse` 函数内改变 `x``y` 后,不会重新触发 `setup` 执行。
2. `x` `y` 拿到的都是 Wrapper 而不是原始值,且这个值会动态变化。

另一个重要 API 就是 **`watch`**,它的作用类似 React Hooks 的 **useEffect**,但实现原理和调用时机其实完全不一样。

`watch` 的目的是监听某些变量变化后执行逻辑,比如当 `id` 变化后重新取数:

```jsx
const MyComponent = {
props: {
id: Number
},
setup(props) {
const data = value(null)
watch(() => props.id, async (id) => {
data.value = await fetchData(id)
})
}
}
```

之所以要 `watch`,因为在 Vue 中,`setup` 函数仅执行一次,所以不像 React Function Component,每次组件 `props` 变化都会重新执行,因此无论是在变量、`props` 变化时如果想做一些事情,都需要包裹在 `watch` 中。

后面还有 `unwatching`、生命周期函数、依赖注入,都是一些语法定义,感兴趣可以继续[阅读原文](https://github.com/vuejs/rfcs/blob/function-apis/active-rfcs/0000-function-api.md#dependency-injection),笔者就不赘述了。

# 3. 精读

对于 Vue 3.0 的 Function API + Hooks 与 React Function Component + Hooks,笔者做一些对比。

## Vue 与 React 逻辑结构

React Function Component 与 Hooks,虽然在实现原理上,与 Vue3.0 存在 Immutable 与 Mutable、JSX 与 Template 的区别,但逻辑理解上有着相通之处。

```ts
const MyComponent = {
setup(props) {
const x = value(0)

const setXRandom = () => {
x.value = Math.random()
}

return { x, setXRandom }
},
template: `
<button @onClick="setXRandom"/>{{x}}</button>
`
}
```

虽然在 Vue 中,`setup` 函数仅执行一次,看上去与 React 函数完全不一样(React 函数每次都执行),但其实 Vue 将渲染层(Template)与数据层(setup)分开了,而 React 合在了一起。

我们可以利用 React Hooks 将数据层与渲染层完全隔离:

```jsx
// 类似 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 的 `setup` 函数也是可选的,再配合其支持的 TSX 功能,与 React 真的只有 Mutable 的区别了:

```jsx
// 这是个 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 的简单语法:

```jsx
const [ count, setCount ] = useState(0)

const setToOne = () => setCount(1)
```

Vue Hooks 的简单语法:

```jsx
const count = value(0)

const setToOne = () => count.value = 1
```

之所以 React 返回的 `count` 是一个数字,是因为 Immutable 规则,而 Vue 返回的 `count` 是个对象,拥有 `count.value` 属性,也是因为 Vue Mutable 规则导致,这使得 Vue 定义的所有变量都类似 React 中 `useRef` 定义变量,因此不存 React `capture value` 的特性。

> 关于 capture value 更多信息,可以阅读 [精读《Function VS Class 组件》 Capute Value 介绍](https://github.com/dt-fe/weekly/blob/v2/095.%E7%B2%BE%E8%AF%BB%E3%80%8AFunction%20VS%20Class%20%E7%BB%84%E4%BB%B6%E3%80%8B.md#capture-props)
另外,对于 Hooks 的值变更机制也不同,我们看 Vue 的代码:

```jsx
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 中,`useMouse` 如果修改了 `x` 的值,那么使用 `useMouse` 的函数就会被重新执行,以此拿到最新的 `x`,而在 Vue 中,将 Hooks 与 Immutable 深度结合,通过包装 `x.value`,使得当 `x` 变更时,引用保持不变,仅值发生了变化。所以 Vue 利用 Proxy 监听机制,可以做到 `setup` 函数不重新执行,但 Template 重新渲染的效果。

这就是 Immutable 的好处,Vue Hooks 中,不需要 `useMemo` `useCallback` `useRef` 等机制,仅需一个 `value` 函数,直观的 Mutable 修改,就可以实现 React 中一套 Immutable 性能优化后的效果,这个是 Mutable 的魅力所在。

## Vue Hooks 的优势

笔者对 RFC 中对 Vue、React Hooks 的对比做一个延展解释:

首先最大的不同:`setup` 仅执行一遍,而 React Function Component 每次渲染都会执行。

**Vue 的代码使用更符合 JS 直觉。**

这句话直截了当戳中了 JS 软肋,JS 并非是针对 Immutable 设计的语言,所以 Mutable 写法非常自然,而 Immutable 的写法就比较别扭。

当 Hooks 要更新值时,Vue 只要用等于号赋值即可,而 React Hooks 需要调用赋值函数,**当对象类型复杂时,还需借助第三方库才能保证进行了正确的 Immutable 更新。**

**对 Hooks 使用顺序无要求,而且可以放在条件语句里。**

对 React Hooks 而言,调用必须放在最前面,而且不能被包含在条件语句里,这是因为 React Hooks 采用下标方式寻找状态,一旦位置不对或者 Hooks 放在了条件中,就无法正确找到对应位置的值。

而 Vue Function API 中的 Hooks 可以放在任意位置、任意命名、被条件语句任意包裹的,因为其并不会触发 `setup` 的更新,只在需要的时候更新自己的引用值即可,而 Template 的重渲染则完全继承 Vue 2.0 的依赖收集机制,它不管值来自哪里,只要用到的值变了,就可以重新渲染了。

**不会再每次渲染重复调用,减少 GC 压力。**

这确实是 React Hooks 的一个问题,所有 Hooks 都在渲染闭包中执行,每次重渲染都有一定性能压力,而且频繁的渲染会带来许多闭包,虽然可以依赖 GC 机制回收,但会给 GC 带来不小的压力。

而 Vue Hooks 只有一个引用,所以存储的内容就非常精简,也就是占用内存小,而且当值变化时,也不会重新触发 `setup` 的执行,所以确实不会造成 GC 压力。

**必须要总包裹 `useCallback` 函数确保不让子元素频繁重渲染。**

React Hooks 有一个问题,就是完全依赖 Immutable 属性。**而在 Function Component 内部创建函数时,每次都会创建一个全新的对象,这个对象如果传给子组件,必然导致子组件无法做性能优化。** 因此 React 采取了 `useCallback` 作为优化方案:

```jsx
const fn = useCallback(() => /* .. */, [])
```

只有当第二个依赖参数变化时才返回新引用。但第二个依赖参数需要 lint 工具确保依赖总是正确的(关于为何要对依赖诚实,感兴趣可以移步 [精读《Function Component 入门》 - 永远对依赖诚实](https://github.com/dt-fe/weekly/blob/v2/104.%E7%B2%BE%E8%AF%BB%E3%80%8AFunction%20Component%20%E5%85%A5%E9%97%A8%E3%80%8B.md#%E6%B0%B8%E8%BF%9C%E5%AF%B9%E4%BE%9D%E8%B5%96%E9%A1%B9%E8%AF%9A%E5%AE%9E))。

回到 Vue 3.0,由于 `setup` 仅执行一次,因此函数本身只会创建一次,不存在多实例问题,不需要 `useCallback` 的概念,更不需要使用 [lint 插件](https://www.npmjs.com/package/eslint-plugin-react-hooks) 保证依赖书写正确,这对开发者是实实在在的友好。

**不需要使用 `useEffect` `useMemo` 等进行性能优化,所有性能优化都是自动的。**

这也是实在话,毕竟 Mutable + 依赖自动收集就可以做到最小粒度的精确更新,根本不会触发不必要的 Rerender,因此 `useMemo` 这个概念也不需要了。

`useEffect` 也需要传递第二个参数 “依赖项”,在 Vue 中根本不需要传递 “依赖项”,所以也不会存在用户不小心传错的问题,更不需要像 React 写一个 lint 插件保证依赖的正确性。(这也是笔者想对 React Hooks 吐槽的点,React 团队如何保障每个人都安装了 lint?就算装了 lint,如果 IDE 有 BUG,导致没有生效,随时可能写出依赖不正确的 “危险代码”,造成比如死循环等严重后果)

# 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 的最大价值。

> 讨论地址是:[精读《Vue3.0 Function API》 · Issue #173 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/173)
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**

> 关注 **前端精读微信公众号**
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">

> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh)

0 comments on commit 30bdcf1

Please sign in to comment.