【RV01】Vue OR React

87 阅读6分钟

前言

React OR Vue 系列

1. Vue Hook OR React Hook

1.1 React Hook 和 Vue Hook 对比

其实 React Hook 的限制非常多,比如官方文档中就专门有一个章节介绍它的限制:

  1. 不要在循环,条件或嵌套函数中调用 Hook
  2. 确保总是在你的 React 函数的最顶层调用他们。
  3. 遵守这条规则,你就能确保 Hook 在每一次渲染中都按照同样的顺序被调用。这让 React 能够在多次的 useState 和 useEffect 调用之间保持 hook 状态的正确。

而 Vue 带来的不同在于:

  1. 与 React Hooks 相同级别的逻辑组合功能,但有一些重要的区别。 与 React Hook 不同,setup 函数仅被调用一次,这在性能上比较占优。
  2. 对调用顺序没什么要求,每次渲染中不会反复调用 Hook 函数,产生的的 GC 压力较小。
  3. 不必考虑几乎总是需要 useCallback 的问题,以防止传递函数prop给子组件的引用变化,导致无必要的重新渲染。
  4. React Hook 有臭名昭著的闭包陷阱问题(甚至成了一道热门面试题,omg),如果用户忘记传递正确的依赖项数组,useEffect 和 useMemo 可能会捕获过时的变量,这不受此问题的影响。 Vue 的自动依赖关系跟踪确保观察者和计算值始终正确无误。
  5. 不得不提一句,React Hook 里的「依赖」是需要你去手动声明的,而且官方提供了一个 eslint 插件,这个插件虽然大部分时候挺有用的,但是有时候也特别烦人,需要你手动加一行丑陋的注释去关闭它。

vue 组合式API里完成 useStateuseMemo 相关工作的 API 并没有通过 useXxx 来命名,而是遵从了 Vue 一脉相承而来的 refcomputed

虽然不符合 react Hook 定义的 Hook 约定,但 vueapi 不按照 react 的约定好像也并没有什么不妥。

我们认可 React Hooks 的创造力,这也是 Vue-Composition-Api 的主要灵感来源。上面提到的问题确实存在于 React Hook 的设计中,我们注意到 Vue 的响应式模型恰好完美的解决了这些问题。

在实际开发中,也遇到了很多问题,尤其是在我想对组件用 memo 进行一些性能优化的时候,闭包的问题爆炸式的暴露了出来。最后我用 useReducer 大法解决了其中很多问题,让我不得不怀疑这从头到尾会不会就是 Dan 的阴谋……(别想逃过 reducer

1.2 原理

从原理的角度来谈一谈两者的区别,

Vue

在 Vue 中,之所以 setup 函数只执行一次,后续对于数据的更新也可以驱动视图更新,归根结底在于它的「响应式机制」,比如我们定义了这样一个响应式的属性:

<template>
  <div>
    <span>{{count}}</span>
    <button @click="add"> +1 </button>
  </div>
</template>

export default {
    setup() {
        const count = ref(0)

        const add = () => count.value++

        return { count, add }
    }
}

这里虽然只执行了一次 setupsetup 是作为一个早于 “created” 的生命周期存在的,无论如何,在一个组件的渲染过程中只会进入一次)但是 count 在原理上是个 「响应式对象」,对于其上 value 属性的改动,

是会触发「由 template 编译而成的 render 函数」 的重新执行的。

如果需要在 count 发生变化的时候做某件事,我们只需要引入 effect 函数:

<template>
  <div>
    <span>{{count}}</span>
    <button @click="add"> +1 </button>
  </div>
</template>

export default {
    setup() {
        const count = ref(0)

        const add = () => count.value++

        effect(function log(){
            console.log('count changed!', count.value)
        })

        return { count, add }
    }
}

这个 log 函数只会产生一次,这个函数在读取 count.value 的时候会收集它作为依赖,那么下次 count.value 更新后,自然而然的就能触发 log 函数重新执行了。

仔细思考一下这之间的数据关系,相信你很快就可以理解为什么它可以只执行一次,但是却威力无穷。

实际上 Vue3 的 Hook 只需要一个「初始化」的过程,也就是 setup,命名很准确。它的关键字就是「只执行一次」。

React

同样的逻辑在 React 中,则是这样的写法:

export default function Counter() {
  const [count, setCount] = useState(0);

  const add = () => setCount((prev) => prev + 1);

  // 下文讲解用
  const [count2, setCount2] = useState(0);

  return (
    <div>
      <span>{count}</span>
      <button onClick={add}> +1 </button>
    </div>
  );
}

它是一个函数,而父组件引入它是通过 <Counter /> 这种方式引入的,实际上它会被编译成 React.createElement(Counter) 这样的函数执行,也就是说每次渲染,这个函数都会被完整的执行一次

useState 返回的 countsetCount 则会被保存在组件对应的 Fiber 节点上,每个 React 函数每次执行 Hook 的顺序必须是相同的,举例来说。 这个例子里的 useState 在初次执行的时候,由于执行了两次 useState,会在 Fiber 上保存一个 { value, setValue } -> { value2, setValue2 } 这样的链表结构。

而下一次渲染又会执行 count 的 useStatecount2 的 useState,那么 React 如何从 Fiber 节点上找出上次渲染保留下来的值呢?当然是只能按顺序找啦。

第一次执行的 useState 就拿到第一个 { value, setValue },第二个执行的就拿到第二个 { value2, setValue2 }

这也就是为什么 React 严格限制 Hook 的执行顺序和禁止条件调用。

假如第一次渲染执行两次 useState,而第二次渲染时第一个 useState 被 if 条件判断给取消掉了,那么第二个 count2 的 useState 就会拿到链表中第一条的值,完全混乱了。

如果在 React 中,要监听 count 的变化做某些事的话,会用到 useEffect 的话,那么下次 render

之后会把前后两次 render 中拿到的 useEffect 的第二个参数 deps 依赖值进行一个逐项的浅对比(对前后每一项依次调用 Object.is),比如

export default function Counter() {
  const [count, setCount] = useState(0);

  const add = () => setCount((prev) => prev + 1);

  useEffect(() => {
    console.log("count updated!", count);
  }, [count]);

  return (
    <div>
      <span>{count}</span>
      <button onClick={add}> +1 </button>
    </div>
  );
}

那么,当 React 在渲染后发现 count 发生了变化,会执行 useEffect 中的回调函数。(细心的你可以观察出来,每次渲染都会重新产生一个函数引用,也就是 useEffect 的第一个参数)。

是的,React 还是不可避免的引入了 依赖 这个概念,但是这个 依赖 是需要我们去手动书写的,实时上 React 社区所讨论的「心智负担」也基本上是由于这个 依赖 所引起的……

由于每次渲染都会不断的执行并产生闭包,那么从性能上和 GC 压力上都会稍逊于 Vue3。它的关键字是「每次渲染都重新执行」