回复 刷新

暂无评论

一文看懂:Vue3 和React Hook对比,到底哪里好?

Vue3 在经过多个开发版本的迭代后,迎来了它的正式版本,,其中最重要的一项RFC就是 Vue Function-based API RFC,很巧的在不久前正好研究了一下react hook,感觉2者的在思想上有着异曲同工之妙,所以有了一个想总结一下关于hook的想法,同时看到很多人关于hook的介绍都是分开讲的,当然可能和vue3.0对于这个特性的说明刚刚问世也有一定的关系。

1、什么是hook?

首先我们需要了解什么是hook,拿react的介绍来看,它的定义是:

它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。在16.8以前的版本中,我们在写react组件的时候,大部分都都是class component,因为基于class的组件react提供了更多的可操作性,比如拥有自己的state,以及一些生命周期的实现,对于复杂的逻辑来讲class的支持程度是更高的:

class Hello extends React.Component { constructor(props) { super(props); this.state = {date: new Date()}; } componentDidMount() { // do sth... } componentWillUnmount() { // do sth... } // other methods or lifecycle... render() { return ( <div> <h1>Hello, world!</h1> <h2>It is {this.state.date.toLocaleTimeString()}.</h2> </div> ); }} constructor(props) { super(props); this.state = {date: new Date()}; } componentDidMount() { // do sth... } componentWillUnmount() { // do sth... } // other methods or lifecycle... render() { return ( <div> <h1>Hello, world!</h1> <h2>It is {this.state.date.toLocaleTimeString()}.</h2> </div> ); } }

同时,对于function component来说,react也是支持的,但是function component只能拥有props,不能拥有state,也就是只能实现stateless component:

function Welcome(props) { return <h1>Hello, {props.name}</h1>;} return <h1>Hello, {props.name}</h1>; }

react 并没有提供在函数组件中设置state以及生命周期的一些操作方法,所以那个时候,极少的场景下适合采用函数组件,但是16.8版本出现hook以后情况得到了改变,hook的目标就是–让你在不编写 class 的情况下使用 state 以及其他的 React 特性,来看个例子:

import React, { useState } from 'react';function Example() { // 声明一个新的叫做 “count” 的 state 变量 const [count, setCount] = useState(0); return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div> );}from 'react'; function Example() { // 声明一个新的叫做 “count” 的 state 变量 const [count, setCount] = useState(0); return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div> ); }

useState就是react提供的一个Hook,通过它我们就可以在function组件中设置自己想要的state了,不仅可以使用还可以很方便的去通过setState(注意不是class中的setState,这里指的是上述例子中的setCount)更改,当然,react提供了很多hook来支持不同的行为和操作,下面我们还会再简单介绍,我们在看下vue hook,这是尤大在vueconf上分享的一段代码:

import { value, computed, watch, onMounted } from 'vue'const App = { template: ` <div> <span>count is {{ count }}</span> <span>plusOne is {{ plusOne }}</span> <button @click="increment">count++</button> </div> `, 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 } }}from 'vue' const App = { template: ` <div> <span>count is {{ count }}</span> <span>plusOne is {{ plusOne }}</span> <button @click="increment">count++</button> </div> `, 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 } } }

从上面的例子中不难看出,和reacthook的用法非常相似,并且尤大也有说这个RFC是借鉴了reacthook的想法,但是规避了一些react的问题,然后这里解释一下为什么我把vue的这个RFC也称为是hook。

因为在reacthook的介绍中有这么一句话,什么是hook–Hook 是一些可以让你在函数组件里“钩入” React state 及生命周期等特性的函数,那么vue提供的这些API的作用也是类似的–可以让你在函数组件里“钩入” value(2.x中的data) 及生命周期等特性的函数,所以,暂且就叫vue-hook吧~

var model = JSON.stringify( model); wx.navigateTo({ url: '../detail/detail?model=' + model, }) //接收 onLoad: function (options) { //将字符串转换成对象 var bean = JSON.parse(options.model); }

设计动机

大如 Vue3 这种全球热门的框架,任何一个 breaking-change 的设计一定有它的深思熟虑和权衡,那么 composition-api 出现是为了解决什么问题呢?这是一个我们需要首先思考明白的问题。

首先抛出 Vue2 的代码模式下存在的几个问题。随着功能的增长,复杂组件的代码变得越来越难以维护。 尤其发生你去新接手别人的代码时。 根本原因是 Vue 的现有 API 通过「选项」组织代码,但是在大部分情况下,通过逻辑考虑来组织代码更有意义。缺少一种比较「干净」的在多个组件之间提取和复用逻辑的机制。类型推断不够友好。

代码组织

Vue 官方给出的自定义 Hook 的例子是这样的:

import { ref, onMounted, onUnmounted } from "vue"; export function useMousePosition() { const x = ref(0); const y = ref(0); function update(e) { x.value = e.pageX; y.value = e.pageY; } onMounted(() => { window.addEventListener("mousemove", update); }); onUnmounted(() => { window.removeEventListener("mousemove", update); }); return { x, y }; } 复制代码

在组件中使用:

import { useMousePosition } from "./mouse"; export default { setup() { const { x, y } = useMousePosition(); // other logic... return { x, y }; }, }; 复制代码

就这么简单,无需多言。在任何组件中我们需要「获取响应式的鼠标位置」,并且和我们的「视图层」关联起来的时候,仅仅需要简单的一句话即可。并且这里返回的 x、y 是由 ref 加工过的响应式变量,我们可以用 watch 监听它们,可以把它们传递给其他的自定义 Hook 继续使用。几乎能做到你想要的一切,只需要发挥你的想象力。

而使用 Hook 以后呢?我们可以把「新建文件夹」这个功能美美的抽到一个函数中去:

function useCreateFolder(openFolder) { // originally data properties const showNewFolder = ref(false); const newFolderName = ref(""); // originally computed property const newFolderValid = computed(() => isValidMultiName(newFolderName.value)); // originally a method async function createFolder() { if (!newFolderValid.value) return; const result = await mutate({ mutation: FOLDER_CREATE, variables: { name: newFolderName.value, }, }); openFolder(result.data.folderCreate.path); newFolderName.value = ""; showNewFolder.value = false; } return { showNewFolder, newFolderName, newFolderValid, createFolder, }; } 复制代码

再来看看被吐槽成「意大利面条代码」的 setup 函数。

export default { setup() { // Network const { networkState } = useNetworkState(); // Folder const { folders, currentFolderData } = useCurrentFolderData(networkState); const folderNavigation = useFolderNavigation({ networkState, currentFolderData }); const { favoriteFolders, toggleFavorite } = useFavoriteFolders(currentFolderData); const { showHiddenFolders } = useHiddenFolders(); const createFolder = useCreateFolder(folderNavigation.openFolder); // Current working directory resetCwdOnLeave(); const { updateOnCwdChanged } = useCwdUtils(); // Utils const { slicePath } = usePathUtils(); return { networkState, folders, currentFolderData, folderNavigation, favoriteFolders, toggleFavorite, showHiddenFolders, createFolder, updateOnCwdChanged, slicePath, }; }, }; 复制代码

2、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 有臭名昭著的闭包陷阱问题,如果用户忘记传递正确的依赖项数组,useEffect 和 useMemo可能会捕获过时的变量,这不受此问题的影响。 Vue 的自动依赖关系跟踪确保观察者和计算值始终正确无误。
  5. 不得不提一句,React Hook 里的「依赖」是需要你去手动声明的,而且官方提供了一个 eslint插件,这个插件虽然大部分时候挺有用的,但是有时候也特别烦人,需要你手动加一行丑陋的注释去关闭它。

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

3、原理

既然有对比,那就从原理的角度来谈一谈两者的区别。

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 但是 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> ); } 复制代码

它是一个函数,而父组件引入它是通过 这种方式引入的,实际上它会被编译成 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。它的关键字是「每次渲染都重新执行」。

结语

Vue hook只会在setup函数被调用的时候被注册一次,react数据更改的时候,会导致重新render,重新render又会重新把hooks重新注册一次,所以react的上手难度更高一些,而vue之所以能避开这些麻烦的问题,根本原因在于它对数据的响应是基于proxy的,这种场景下,只要任何一个更改data的地方,相关的function或者template都会被重新计算,因此避开了react可能遇到的性能上的问题

当然react对这些都有解决方案,想了解的同学可以去看官网有介绍,比如useCallback,useMemo等hook的作用,我们看下尤大对vue和react hook的总结对比:

整体上更符合 JavaScript 的直觉;

  1. 不受调用顺序的限制,可以有条件地被调用;
  2. 不会在后续更新时不断产生大量的内联函数而影响引擎优化或是导致 GC 压力;
  3. 不需要总是使用 useCallback 来缓存传给子组件的回调以防止过度更新;
  4. 不需要担心传了错误的依赖数组给useEffect/useMemo/useCallback 从而导致回调中使用了过期的值 —— Vue的依赖追踪是全自动的。

不得不说,青出于蓝而胜于蓝,vue虽然借鉴了react,但是天然的响应式数据,完美的避开了一些react hook遇到的短板~

  • 88
  • 0
  • 0