Vue转React随记

3,140 阅读4分钟

前言

  • 从技术栈Vue转为React是很常见的情况,本文就该阶段做一个汇总记录。
  • 直接学习React的新人开发者也可作为参考。
  • 本文中的内容大多为个人碰到的问题或者思考。这其中绝大部分在官网都有说明。

Vue到React

本文只记录最重要,最明显的直观差异。如果想要详细的功能对比,推荐大家看这篇文章

  • 如果你使用过Vue的渲染函数和JSX,这部分内容和React是相似的。而在React中这部分的比例更重。因为Vue大多数时候推荐使用模板语法。但要注意只是相似,JSX编译并不完全一样。差异点查看官网这里
  • React不会自动帮你更新视图,组件内部数据变更后,需要手动setState。如果传入子组件的props发生变化,子组件会触发重新渲染。
  • React中的props比Vue更灵活。几乎可以看做是Vue的props + slot。

常见问题

引发死循环

1. 组件中直接调用会触发re-render的逻辑

export const MyComponent = (props) => {
  const [t, setT] = useState(0);
  // 每次组件渲染都会重新走到这里,setState又重新触发组件渲染
  setT((o) => o + 1);
  return <h1>{t}</h1>;
};

2. useEffect无依赖时调用re-render的逻辑(和1类似)

export const MyComponent = (props) => {
  const [t, setT] = useState(0);
  // 因为无依赖,组件每次渲染完成后会进入useEffect的回调,setState又重新触发组件渲染
  useEffect(() => {
    setT((o) => o + 1);
  });
  return <h1>{t}</h1>;
};

3. 不恰当设置props默认值

这一点是最隐蔽,最容易忽视的,一定要非常小心。

export const MyComponent = (props) => {
  // arr是一个引用类型,引用类型提供默认值,每一次渲染时都赋了一个不同的地址引用。
  const { arr = [] } = props;
  const [t, setT] = useState(0);
  // useEffet会认为props arr发生了变化,所以每次渲染都会进入回调,实际上和情况2一样了
  useEffect(() => {
    setT((o) => o + 1);
  }, [arr]);
  return <h1>{t}</h1>;
};

函数中取到的值未更新

1. setState异步更新

export const MyComponent = (props) => {
  const [t, setT] = useState(0);
  useEffect(() => {
    setT((o) => o + 1);
    // setState后马上同步获取,获取到的为更新前的值
    console.log(t, 't'); // 0
  }, []);
  return <h1>{t}</h1>;
};

2. Hooks闭包陷阱

对于setState异步更新中的例子,再来扩展一下,可以发现useState的闭包问题。

export const MyComponent = (props) => {
  const [t, setT] = useState(0);
  useEffect(() => {
    setT((o) => o + 1);
    // 既然是时序问题,就强行延后执行state获取操作,会发现依然取不到更新后的state
    setTimeout(() => {
      console.log(t, 't0'); // 0
    }, 1000);
  }, []);
  return <h1>{t}</h1>;
};
  • 产生原因:简单的讲,例子中的effect只会在首次渲染完后被执行,effect函数在执行时创建自己的执行上下文,变量t因为闭包的原因一直存在于该执行上下文中,当第二次渲染,t更新了,实际上和effect中的t在内存中是两个变量。如果在依赖列表中加入t,useEffect内部会自动更新变量t。

3. 解决方法

问题1、2的详细原因和完善的解决方案,可参考这篇文章。这里给出个人最常用的解决方案。

  • 在useEffect中,如果逻辑允许,可以把变量加入到依赖列表中。比如对于上述例子,如果你只需要获取t,并且打印,就可以将其加入到依赖列表。而例子中即要get,又要set。如果加入到依赖列表会引发死循环
  • 使用useRef。useRef创建的变量贯穿于组件整个生命周期。所以不存在闭包问题。

容易忽视的概念

这部分是个人在通读官网后,发现和我之前理解存在偏差的一些概念。

useEffect的清除时机

  1. 执行下一个 effect 之前,上一个 effect 就已被清除。即useEffect的回调被执行前,清除上一次的回调。
export const MyComponent = (props) => {
  const [t, setT] = useState(0);
  useEffect(() => {
    if (t < 5) {
      setT((o) => o + 1);
    }
    // 在return的函数中执行清除的逻辑
    return () => {
      console.log(t); //打印了5次,0 1 2 3 4
    };
  });
  return <h1 onClick={() => {}}>{t}</h1>;
};
  1. 组件卸载时一定会调用一次清除。这也是为什么能够使用依赖为空数组的useEffect代替组件卸载的生命周期

JSX对于不同数据结构的处理方式

  • 布尔类型、Null 以及 Undefined 将会忽略
// 可以放心大胆的写如下判断代码,不必担心true或者undefined被渲染到页面上。但要小心0和NaN
{t && <h1>{t}</h1>}
  • 数组会被解构渲染,即按顺序渲染数组中的每一项

动态组件

如果某个场景下的组件需要动态渲染,可以有两个方案。

  • 先将组件名赋值给一个大写的变量,必须大写,然后使用JSX渲染。这里展示官网的例子
import React from 'react';
import { PhotoStory, VideoStory } from './stories';

const components = {
  photo: PhotoStory,
  video: VideoStory
};

function Story(props) {
  // 正确!JSX 类型可以是大写字母开头的变量。
  const SpecificStory = components[props.storyType];
  return <SpecificStory story={props.story} />;
}
  • 直接使用createElement方法创建组件,该方法的第一个参数可以使用正常的变量。所以改造官网例子。
import React, { createElement } from 'react';
import { PhotoStory, VideoStory } from './stories';

const components = {
  photo: PhotoStory,
  video: VideoStory
};

function Story(props) {
  // c为js变量即可
  const c = components[props.storyType];
  return createElement(c, { story: props.story });
}