本文已参与「新人创作礼」活动,一起开启掘金创作之路。详情
本篇文章知识点速览:
- 函数组件中使用类组件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继承的问题。
- 在这里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来访问
-
super只能在子类派生类的构造函数和静态方法中使用,不能在super之前引用this
-
如果没有定义类构造函数,在实例化派生类时会调用super,而且会传入所有传给派生类的参数。
-
super也可以作为对象使用,但是不能单独使用
constructor(props) {
super(props)
console.log('父类的setState方法', super.setState)
// 但是不能直接super,可以查看super的某些属性/方法
}
打印的setState即为源码中的Component的setState
函数组件中的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规则)
- 只能在最顶层使用HOOK,不要再循环/条件/嵌套函数中调用hook
- 只在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
【拓展memoizedState】
① 在函数组件上的memoizedState保存的是hook链表
② class组件上的memoizedState存的是组件实例
③ 在原生标签节点上的memoizedState存的是dom节点
函数组件和类组件怎么选择使用呢?
函数组件和类组件都可以取代对方,只是采用的实现方法不同。具体有:
1、颗粒度
函数组件的颗粒度更小,是函数式编程的优先选择
颗粒度体现是state定义与useEffect、useLayoutEffect上。函数组件可以写多个,也可以自定义hook调用。
相比之下,类组件的state和每个生命周期在组件中只能使用一次。组件拆分也比较繁琐。
2、实例
类组件有实例,函数组件没有实例。如果需要使用实例,可以首选类组件。
3、复用状态逻辑
函数组件和类组件都可以复用状态逻辑。
类组件可以通过hoc高阶组件、render props等方法复用,但是注意容易造成嵌套地狱。(参考antd3使用Form组件还是要数据管理的connect就很多层嵌套)
函数组件可以使用自定义hook来实现复用
4、学习成本
类组件中有相关this指向问题,合成事件需要理解成本。函数组件上手比较简单。