前言
React OR Vue 系列
1. Vue Hook OR React Hook
1.1 React Hook 和 Vue Hook 对比
其实 React Hook 的限制非常多,比如官方文档中就专门有一个章节介绍它的限制:
- 不要在循环,条件或嵌套函数中调用 Hook
- 确保总是在你的 React 函数的最顶层调用他们。
- 遵守这条规则,你就能确保 Hook 在每一次渲染中都按照同样的顺序被调用。这让 React 能够在多次的 useState 和 useEffect 调用之间保持 hook 状态的正确。
而 Vue 带来的不同在于:
- 与 React Hooks 相同级别的逻辑组合功能,但有一些重要的区别。 与 React Hook 不同,
setup函数仅被调用一次,这在性能上比较占优。 - 对调用顺序没什么要求,每次渲染中不会反复调用 Hook 函数,产生的的 GC 压力较小。
- 不必考虑几乎总是需要 useCallback 的问题,以防止传递
函数prop给子组件的引用变化,导致无必要的重新渲染。 - React Hook 有臭名昭著的闭包陷阱问题(甚至成了一道热门面试题,omg),如果用户忘记传递正确的依赖项数组,useEffect 和 useMemo 可能会捕获过时的变量,这不受此问题的影响。 Vue 的自动依赖关系跟踪确保观察者和计算值始终正确无误。
- 不得不提一句,React Hook 里的「依赖」是需要你去手动声明的,而且官方提供了一个 eslint 插件,这个插件虽然大部分时候挺有用的,但是有时候也特别烦人,需要你手动加一行丑陋的注释去关闭它。
vue 组合式API里完成 useState 和 useMemo 相关工作的 API 并没有通过 useXxx 来命名,而是遵从了 Vue 一脉相承而来的 ref 和 computed。
虽然不符合 react Hook 定义的 Hook 约定,但 vue 的 api 不按照 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 }
}
}
这里虽然只执行了一次 setup (setup 是作为一个早于 “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 返回的 count 和 setCount 则会被保存在组件对应的 Fiber 节点上,每个 React 函数每次执行 Hook 的顺序必须是相同的,举例来说。 这个例子里的 useState 在初次执行的时候,由于执行了两次 useState,会在 Fiber 上保存一个 { value, setValue } -> { value2, setValue2 } 这样的链表结构。
而下一次渲染又会执行 count 的 useState、 count2 的 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。它的关键字是「每次渲染都重新执行」。