互联网寒冬将至,很多大厂招前端不仅对react有要求,而且要精通才可以。本文就手摸手带你从根本上理解React Hooks并写出一个自己的React Hooks。
React Hooks 原则
使用过React Hooks的同学都知道,React建议开发者在使用Hooks的时候遵循以下两个原则:
- 只在React函数中使用hooks。
- 不要在循环、判断条件或嵌套函数中使用hooks。 第一个原则的意思是,我们要在参与渲染过程的函数中使用hooks,这个应该很简单,但是第二个原则是什么一意思呢?仔细想想?如果在循环、判断条件或者嵌套函数中调用hooks的话会发生什么呢?没错,会导致hooks前后两次调用的顺序不一致。也就是说我们要保证每次渲染,Hooks的调用顺序保持一直才可以。
到这里有同学就想发出疑问了,为什么非要有这两个原则呢?其实这两个原则离不开hooks的原理。
Hooks 的原理
hooks的原理就涉及到渲染过程了,但是渲染过程又不得不提及fiber,提及fiber又要劝退一部分人。但是想要理解hooks的原理的话我认为不必非要提及fiber,接下来我将用不涉及fiber的抽象方式给你讲清楚hooks的工作方式。
拿下面的代码举例
function A() {
console.log('A render');
let [target, setTaget] = useState(1);
if (target % 2 === 0) {
return (
<button
onClick={() => {
setTaget(target + 1);
}}
>
target+1
</button>
);
}
return (
<>
<button
onClick={() => {
setTaget(target + 1);
}}
>
target+1
</button>
<B></B>
</>
);
}
function B() {
console.log('B render');
let [state, setState] = useState(0);
return (
<>
<button
onClick={() => {
setState(state + 1);
}}
>
state+1
</button>
<div>{state}</div>
</>
);
}
ReactDOM.render(<A />, document.getElementById('root'));
这个代码渲染出来是这个样子的:
此时对应的虚拟DOM节点是这样的
当点击target+1按钮时会先把B组件隐藏,再次点击按钮,B组件显示。
点击state+1按钮会使下面的数字+1
然后我们想象一下我们如果进行这样的一系列操作,在hooks层面都做了什么:
1.点击state+1。
当我们点击state+1按钮的时候,我们会再得到一个虚拟DOM树,但是在得到这个虚拟DOM树的过程中,我们就需要一些思考了。
不知道你们心中有没有跟我类似的疑问,我刚开始使用useState的时候,就很郁闷,函数的上下文在运行完毕后就立即被回收了,但是为什么useState可以保存上一次渲染的状态。
其实react是通过给组件节点加上一个数组指针来记录hooks的,当组件在渲染期间,所有调用的hooks都会按照顺序地被记录,并放入这个数组中,这也是为什么我们使用hooks时需要保证hooks调用顺序保持不变,因为一但顺序发生改变,那么hooks内部的引用就会出错。
理解了这个之后接下来我们再看完整的图
这个时候就很好理解了,当我们点击state+1按钮之后,在渲染到组件B时,我们可以通过全局的这么一个指针去找到上一次渲染时hooks保留的状态,这次再按照顺序调用hooks,我们只需要在对应的保留hooks调用记录的数组上按照从头到尾的顺序找一下就好了。
2.点击target+1。
当我们点击target+1的时候,就又会发生一些变化,不过这些变化也又印证了前面讲的原理是对的。
根据代码可以知道点击target+1之后组件B会被从DOM树中移出。此时页面的虚拟DOM结构是这样的。
因为新生成的虚拟DOM树上已经没有有关组件B的内容了,因此真正渲染到真实DOM上自然也就没有了。但是这里会发现,跟随组件消失的还有保留hooks调用记录的数组,因为组件已经被卸载了,保留状态无意义了。
3.点击target+1。
再次点击target+1组件又回来了,但是是一个全新的组件,之前的状态已经无法找回了,因为保存状态的数组也是一个全新的。
此时DOM结构对比是这样的
总结
原生hooks是通过数组形式来按照hooks的调用顺序来保留调用记录和状态的。
Hooks实战
因为我这篇文章的话,目的不是教你怎么用官网的hooks API,学API的话还是去官网看,又有中文版的,多好。我是为了教会大家如何在日常工作中,根据业务逻辑封装自己的Hooks。造起来。
在开始实战前我还是想啰嗦一下,虽然我们前面理解了原理,但是还是要明白,Hooks现在有了什么能力,是个什么位置。
Hooks本质上是一个函数调用,之所以可以封装业务逻辑是因为有了原生Hooks可以记住状态并触发视图更新支持的能力(当然也支持了其他能力,只是这个最适合拿来举例),如果没有这个能力支持,那么我们自己写的Hooks就是一个函数调用,很难调动react的能力。
记录mount时间的hooks
这个简单函数的应用于埋点和行为监控,可以记录我们从页面mount到操作的时间。一般这种计时的逻辑需要在组件挂载逻辑写,有了hooks之后就可以直接把逻辑封装到里面了,这归功于useEffect的神奇魔力。
上代码
import React, { useEffect, useRef } from 'react';
const useInterval = () => {
const time = useRef(0);
useEffect(()=>{
// mount逻辑
time.current = Date.now()
},[])
return Date.now() - time.current
}
这样在一个组件中直接调用useInterval方法就会返回从第一次mount到再次调用的时候的毫秒数,代码很简单,但是可以反映出hooks复用逻辑的潜力。
一个抽象的逻辑复用例子
hooks在面对需要卸载的逻辑的时候,显得更加灵活和方便,我们经常因为一些需要在unmount的时候调用一些钩子而觉得很麻烦。
直接上例子,这次的例子是抽象的伪代码,只是为了让大家理解思想。
假如我们需要订阅websocket,并实时获取websocket推送的第一条信息,那么我们就可以使用hooks的能力进行封装
import React, { useEffect, useRef, useState } from 'react';
const useWebsocket = () => {
const [data, setData] = useState(null);
const flag = useRef(null)
useEffect(()=>{
// 模拟mount的时候订阅逻辑
flag.current = subscribe('***',(data)=>{
// 模拟数据的回调
setData(data)
})
// 模拟卸载的逻辑
return () =>{
flag.current.unSubscribe
}
})
return data
}
总结
了解原理是为了更好的使用api,hooks的最大进步就是可以灵活地复用逻辑,越大的项目对于效率的提升越明显。
另外最后加个广告,字节跳动电商部门内推招聘,校招社招都有感兴趣的可以加我微信(zs15931442916)或者扫描下方二维码。
其他部门也可以推,只是给自己部门打个广告。
不投简历也可以加好友,互相聊一聊也挺好,可以帮忙看看简历啥的,有啥学习的问题互相讨论讨论。