likes
comments
collection
share

Vue快速转React指南(三)

作者站长头像
站长
· 阅读数 33

前置说明

已发布:

本篇要点

  • React的Hooks

开始

熟悉Vue的话,组件的用途和概念大家都知道,这里主要讲react的组件和Vue的不同。 不知道组件的概念的建议去Vue官方文档复习一下

React Hooks

  • Vue组件:单文件组件(SFC)
  • react组件:函数式组件

Vue官方文档-SFC这一节专门对「单文件组件」进行了很长篇幅的介绍,简单来说就是.vue文件,将JS代码、template和CSS混合在一起。那么,template组件需要的数据就可以从当前的script里去取。

<script setup>
import { ref } from "vue";

defineProps({
  msg: {
    type: String,
    required: true
  }
});
const count = ref(0); // 定义count初始值
</script>

<template>
  <div class="greetings">
    <h1 class="green">{{ msg }}</h1>
    <h2>{{ count }}</h2>
  </div>
</template>

对于react来说,react的组件就是一个JS函数,它返回你声明的UI代码:

function App (props) {
    return <h1> Hello, {props.name} </h1>
}
export default App

函数式组件的编写规范要求是一个纯净的函数(纯函数 pure fucntion),除了纯净的输入输出没有函数副作用(side effect)

副作用函数:指的是函数的运行会对其他地方的变量产生影响的函数。 比如你的函数修改了全局变量、修改了另一个函数也能修改的变量等行为,都称为副作用函数。

function effect(){
    document.body.innerText = 'effect';
}

上面的effect就是一个副作用函数,因为它会修改到全局都可以获取和修改的变量。

所以,对于react的函数式组件来说,要符合纯函数的标准,输入是props,函数体内只做数据处理,输出是return出去的HTML实现的UI代码。

但是很多时候,我们都需要在组件内进行一些副作用操作,比如:存储数据、改变应用全局状态等等。这个时候就需要React的Hooks来解决。

Hook的中文意思为钩子🪝,叫这个名字的寓意是:如果需要外部功能和副作用,就用钩子把外部需要的东西"钩"进来

常用的Hook

在react里,hook的规范是用use开头,后面跟上你对这个hook作用的描述。

下面讲几个常用的hook来加深记忆

  • useState
  • useEffect
  • useMemo

useState状态钩子

useState是react里最常见的一个hook,用于为函数组件引入状态(state)。纯函数不能有状态,所以把状态放在钩子里面。

import { useState } from 'react';

function ButtonText() {
  const [text, setText] = useState('hello World');

  const clickButton = () => setText('click Button');

  return (
    <button onClick={clickButton}>{text}</button>
  );
};

export default ButtonText;

在别的组件里可以直接使用:

import ButtonText from './feature/ButtonText'
function App() {
    return <ButtonText />
}

在Vue里我们这样实现:

<script setup>
import { ref } from 'vue';

const text = ref('hello world');
const clickButton = () => {
  text.value = 'click Button';
};
</script>

<template>
  <button @click="clickButton">{{text}}</button>
</template>

看到Vue的实现,有的朋友肯定心里就会想了,为什么Vue可以直接修改状态值,而react就必须用useState导出的方法函数?

把react里的代码改成vue那种方式之后,会发现点击了之后,点击事件是执行了,但是界面的text没有发生改变

import { useState } from 'react';

function ButtonText() {
  const [text, setText] = useState('hello World');

  const clickButton = () => {
    console.log('click生效');
    text = 'click Button'; // 直接修改
  };

  return (
    <button onClick={clickButton}>{text}</button>
  );
};

export default ButtonText;

这里涉及到原理问题,这篇不打算深入讲。简单来说就是React没有Vue框架里的响应式系统(reactive),所以你没有用setText去修改text,react底层会认为这个text并没有改变,数据没有改变自然也不会去更新视图。

Vue里申明一个响应式变量需要使用ref包裹,那么只要ref里的数据改变,Vue框架底层利用proxy原理是可以追踪到的,这样就可以做到框架底层在数据变化时重新渲染视图,不需要框架使用者去决定当前组件需不需要更新,所以框架使用者只需要修改数据就好,降低了很多的心智负担。

这里我们可以看出,react并不reactive,Vue也不是只关注view,怀疑这俩框架是不是名字取反了....

那还是我前面那句老话,react没有实现reative的部分,就需要框架使用者去实现。这个会在后面的文章细讲。

useEffect:副作用钩子

useEffect的作用就是为了执行一些副作用相关的逻辑。比如:异步请求数据等。

使用说明:useEffect(effect: React.EffectCallback, deps?: React.DependencyList | undefined): void

会接收两个参数,第一个是一个必传的副作用的callback函数,第二个是可选值,是一个依赖列表。

import { useEffect, useState } from 'react';

function Test() {
  const [text, setText] = useState('hello World');
  const clickButton = () => setText('click Button');

  useEffect(() => {
    if (text === 'hello World') {
      // 执行拉取数据
    }
  }, [text]);

  return (
    <button onClick={clickButton}>{text}</button>
  );
};

export default Test;

useEffect会在第一次组件DOM挂载的时候执行一次,之后每一次执行是看传入的依赖列表里的变量,只要里面的变量数据变化就会重新执行一次。

如果不传入依赖的话,useEffect只会在第一次DOM挂载时执行,之后组件再重新渲染也不会执行。 因为不传入依赖项代表当前的副作用函数不依赖任何变量,所以那些变量无论怎么变,副效应函数的执行结果都不会改变,所以运行一次就够了。

// 返回值
useEffect(() => {
  const subscription = props.source.subscribe();
  return () => {
    subscription.unsubscribe();
  };
}, [props.source]);

useEffect()允许返回一个函数,在组件卸载时,执行该函数,清理副效应。如果不需要清理副效应,useEffect()就不用返回任何值。

useMemo:存储钩子

Memo是memory(记忆)的简写,useMemo的作用是在再次渲染前存储这一次的渲染结果,为了减少同一个结果渲染很多次带来的性能开销。

import { useMemo } from 'react';  

function TodoList({ todos, tab, theme }) {

  // const visibleTodos = filterTodos(todos, tab);
  const memoVisibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);  
  // 在JSX里使用 memoVisibleTodos
  // ...
}

filterTodos函数的作用就是从传入的todos里筛选出需要的结果。useMemo缓存了最终筛选出的结果。

useMemo需要传入两个参数:

  • 一个没有参数的计算函数,就像上面的() =>,函数返回的是函数计算的结果。
  • 一个依赖项列表,包含函数在计算中使用的每个值。👆🏻使用到了todostab,所以传入了[todos, tab]

useMemo vs. computed

Vue里的computed计算属性和useMemo的作用很像,但是computed并不需要你输入依赖项,因为底层的响应式系统可以自动分辩出哪些变量是依赖项,然后依赖项变化才会返回新的值。

// vue2
export default {
  computed: {
    visibleTodos() {
      return filterTodos(this.todos, this.tab)
    }
  }
}

// vue3
import { computed } from '@vue/reactivity';

// ...
setup(props) {
 const visibleTodos = computed(() => filterTodos(props.todos, props.tab));
}

看到这里,是不是觉得除了依赖项computeduseMemo是一样的呢?

其实因为底层实现的不同,useMemo比起computed坑很多,主要是依赖项是对象和数组的情况下。下面细说一下:

function Dropdown({ allItems, text }) {  
  const searchOptions = { matchMode: 'whole-word', text };  
  
  const visibleItems = useMemo(() => {  
    return searchItems(allItems, searchOptions);  
}, [allItems, searchOptions]);

// ...

上面的useMemo是不生效的,因为每一次render的时候,无论text值和缓存的是否一样,searchOptions对象在react里都被认为是不一样的

react里判断依赖项里的变量是否和缓存的一样,是通过Object.is()这个方法,这个方法就是一个浅层的比较,类似于对变量进行===这种比较。在JS里,对象和数组不属于基本数据类型,属于引用类型,所以{}{}[][]并不相等。

回到上面的代码,对于Object.is({ matchMode: 'whole-word', text },{ matchMode: 'whole-word', text }),哪怕text的值是一样的返回的结果也是false

因此,上面的useMemo在组件每次重新渲染的时候都会重新计算,因为依赖项里的searchOptions每次都会被判定为不相等。

下面是正确的写法:

需要对对象再套一层useMemo

function Dropdown({ allItems, text }) {  
  const searchOptions = useMemo(() => {  
    return { matchMode: 'whole-word', text };  
  }, [text]); // ✅ 只在text改变的时候改变

  const visibleItems = useMemo(() => {  
    return searchItems(allItems, searchOptions);  
  }, [allItems, searchOptions]);
// ...

在Vue里判断一个变量是否改变是通过响应式系统里的setter,并且可以做到对象和数组递归都加上响应式,就不会出现{} !== {}的现象,这样的话对于Vue来说对象{ matchMode: 'whole-word', text },只要text是一样的,那么表示这个变量是没有改变的,框架底层就不会重新渲染。这一切都是框架底层所做的,对于使用者来说就是关注变量在业务里的作用就行了,不需要关心这些底层原理。

所以这又是响应式系统带来的心智负担。

最后

回顾本篇文章要点:

  1. 讲了react hooks的概念和用法
  2. 讲了几个常用hook:useState/useEffect/useMemo

React的官方文档里没有对函数组件有太多的介绍和解释,所以这里推荐阮一峰的react-hooks教程

预告:下一篇开始从一个blog的demo开始讲redux