# 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 与 Mutable 深度结合，通过包装 `x.value`，使得当 `x` 变更时，引用保持不变，仅值发生了变化。所以 Vue 利用 Proxy 监听机制，可以做到 `setup` 函数不重新执行，但 Template 重新渲染的效果。

这就是 Mutable 的好处，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)）
