React使用中遇到的一些问题整理

1,154 阅读7分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。详情

本篇文章知识点速览:

  • 函数组件中使用类组件forceUpdate类似方法
  • 类组件中constructor为什么一定要用super
  • 函数组件中的setState没有回调怎么办?
  • useState与useReducer为什么返回的是一个数组
  • (详解HOOK规则) HOOK为什么只能用在React函数的最顶层
  • 函数组件和类组件怎么选择使用呢?

函数组件想用forceUpdate怎么办

我们都知道类组件中有forceUpdate可以强制更新页面。但是函数组件中的state或props没有变更时我们想实现类似的强制更新怎么办呢?

方法1:使用useReducer 或 额外定义一个state

export default function() {
  // const [state, forceUpdate] = useState(0)
  const [,forceUpdate]= useReducer(x=>x+1, 0)
  console.log('update')
  const btnClick = () => {
      // forceUpdate(state+1)
      forceUpdate()  // useReducer可以不用每次调用forceUpdate还有传值
  }
  return (<div>
      <button onClick={btnClick}>btn</button> 
  </div>)
}

我们每次点击按钮,就可以实现界面的重新渲染了。每点一次按钮就打印一次'update'。

但是我们需要forceUpdate的话每次都需要定义一个useReducer,太麻烦,我们试着把它抽离封装一下:

方法2:自定义HOOK

// 定义一个useForceUpdate方法
function useForceUpdate() {
    const [state, setState] = useState(0) // 同样可以使用useReducer自定义一个hook
    // 使用useCallback缓存一下函数,否则每次的update都是新定义的函数
    const update = useCallback(() => {
        setState(prev=>prev+1)
    }, []);
    return update;
}
export default function() {
  const forceUpdate = useForceUpdate()
  const btnClick = () => {
    forceUpdate()
  }
  return (<div>
      <button onClick={btnClick}>btn</button> 
  </div>)
}

类组件中constructor为什么一定要用super

首先这个问题并不是React的知识点,而是ES6中class继承的问题。

  1. 在这里super作为函数调用,代表父类的构造函数。ES6规定,子类的构造函数必须执行一次super函数。否则会报错。只有调用一下父类的构造函数(super)才能实现继承。
class Demo extends React.Component {
  constructor(props) {
    debugger;
    super(props)
    console.log('this', this)
  }
  render() {
    return <div>{this.props.name}</div> 
  }
}
const jsx = <ClassComponent name='class组件' />
export default jsx;

在debugger的时候,我们看到这时进到了源码中的Component函数中,即父类的构造函数。此处React.Component的构造函数可以接收props、context、updater。

所以给super传了props、context这些参数,我们就可以在子组件中通过this.props、this.context来访问

image-20220223165223055.png

image-20220223165258612.png

  • super只能在子类派生类的构造函数和静态方法中使用,不能在super之前引用this

  • 如果没有定义类构造函数,在实例化派生类时会调用super,而且会传入所有传给派生类的参数。

  • super也可以作为对象使用,但是不能单独使用

constructor(props) {
    super(props)
    console.log('父类的setState方法', super.setState)
    // 但是不能直接super,可以查看super的某些属性/方法
}

打印的setState即为源码中的Component的setState

image-20220223163924274.png

函数组件中的setState没有回调怎么办?

我们知道在类组件中的this.setState可以接收第二个参数做回调。在这个回调中可以拿到state更新后的值。

那么我们在函数组件中怎么做呢?

我们可以使用useEffect,将需要监听的state放入依赖项,然后在依赖项的state变化后就会使用回调了。

另外还有useLayoutEffect可以类似useEffect使用。两个hook的签名是一样的,只是执行的时机不同。useEffect是在dom更新后延迟调用,是异步。useLayoutEffect是在更新dom时同步调用。

具体可见useLayoutEffect

useState与useReducer为什么返回的是一个数组不是对象

答:为了用户自定义值

因为我们在一个组件中会用到多次useState/useReducer。或者在不同地方用到多次。如果我们返回一个对象,其中包含state值和修改的方法,那势必这个对象的key值就写死了。我们用多次的时候就很不方便,当然我们也可以将固定key值名修改成自定义的变量名,但这样每个useState都要多操作一步。所以源码为了方便直接返回数组,我们可以自行定义状态变量名。

function Demo() {
  const [num, setNum] = useState(0)
  // 如果返回的是一个对象,我们需要用定义一个对象名来接受
  // const {state, setState} = useState(0)
  // const num = state  // 需要自己改名定义
  return (<div>
    <button onClick={()=>setNum(num+1)}>{num}</button>
  </div>)
}

HOOK为什么只能用在React函数的最顶层 (详解HOOK规则)

React HOOK规则

  1. 只能在最顶层使用HOOK,不要再循环/条件/嵌套函数中调用hook
  2. 只在React函数或自定义hook中调用HOOK,不要在普通js函数中调用hook

例如:

function Demo() {
    const [count, setCount] = useState(0)
    useEffect(()=>{
      console.log(count)
    }, [count])
    return(<div>
      <button onClick={()=>setCount(count+1)}>{count}</button>
    </div>)
}

1、第2个规则:hook的保存问题

① 此处count每次按钮点击时,都在上一次的基础上+1,那么我们肯定要把上一次的值存起来,才能在下一次的变更时累加。

② 我们都知道state变化时React回去diff新老组件,重新渲染。上面的useEffect也是监听count跟上一次的值比对。那么我们势必要将state值存起来,以此来进行新旧节点的对比。那么我们把这些值存到哪里呢?

③ 我们也知道React中使用了虚拟dom对象,那不就可以存到虚拟对象上了。也就是Fiber节点

React在Fiber上存了函数组件的state、effect等待这些hook。

所以我们可以在函数组件中调用hook。自定义hook中使用的hook最终还是放在函数组件中,所以我们也可以在自定义hook的函数中调用。 而普通的js函数,并不是组件,没有Fiber节点,所以无法使用hook。

2、第1个规则:hook的顺序稳定性问题

在上面一个问题中(useState返回的是数组不是对象问题),我们知道了hook是没有变量名的,那么我们怎么区分两次调用的相同hook呢?

例如两个useState:

function Demo() {
    const [count, setCount] = useState(0)  // 看着hook0
    const [name, setName] = useState('x')   // 看作hook1
    useEffect(()=>{
      console.log(count)
    }, [count])  // hook2
    return(<div>
      <button onClick={()=>setCount(count+1)}>{count}</button>
      <button onClick={()=>setName(name+'x')}>{name}</button>
    </div>)
}

① 比如上面示例的两次调用useState,我们知道在源码中并没有命名去区分,但是我们在组件中两次的useState代表两个不同的状态值。我们怎么区分哪个hook是哪个呢?所以这些hook调用的顺序就很重要。

② React源码中是使用了链表结构还保存这一串的hook的。我们是将这些值存到了Fiber节点的fiber.memoizedState的属性上。所以最终的形式就是fiber.memoizedState(hook0)(头节点) -> next(hook1) -> next(hook2)

③ 所以如果我们在if(xx)条件语句中使用hook,就不能保证hook顺序的稳定性。如果是if (xx) {useState(count)}那当xx为true时头节点就是这个hook,如若是false,那下面的name的hook就成了头节点。那我们两次的新老节点的状态值就无法正常diff了。

④ 循环语言同理,我们的循环次数不固定,跳出循环的时机也不固定,所以创建了多少个hook、有没有创建hook都是不确定的,无法保证hook的顺序稳定性。同理在嵌套函数和条件语句类似,无法保证顺序。

我们看一下Fiber节点上的memoizedState

Fiber节点

【拓展memoizedState

① 在函数组件上的memoizedState保存的是hook链表

② class组件上的memoizedState存的是组件实例

③ 在原生标签节点上的memoizedState存的是dom节点

函数组件和类组件怎么选择使用呢?

函数组件和类组件都可以取代对方,只是采用的实现方法不同。具体有:

1、颗粒度

函数组件的颗粒度更小,是函数式编程的优先选择

颗粒度体现是state定义与useEffect、useLayoutEffect上。函数组件可以写多个,也可以自定义hook调用。

相比之下,类组件的state和每个生命周期在组件中只能使用一次。组件拆分也比较繁琐。

2、实例

类组件有实例,函数组件没有实例。如果需要使用实例,可以首选类组件。

3、复用状态逻辑

函数组件和类组件都可以复用状态逻辑。

类组件可以通过hoc高阶组件、render props等方法复用,但是注意容易造成嵌套地狱。(参考antd3使用Form组件还是要数据管理的connect就很多层嵌套)

函数组件可以使用自定义hook来实现复用

4、学习成本

类组件中有相关this指向问题,合成事件需要理解成本。函数组件上手比较简单。