现在React 16+已经是主流并且挺成熟了,如果还不知道啥是hooks,没用过hooks,或者只知道它是钩子,已经很难在面试时说服面试官选择自己了。
所以很有必要搞清楚,到底啥玩意是钩子,它解决了什么,到底哪里好,它们都是如何实现的?
带着这样的问题,一起学习一下React常用 的hooks。
什么时候使用hooks,它有什么好处?
- 类组件可复用性差 (高阶组件复用,多层复用)
- 类组件性能稍差 (需要维护类实例)
- 生命周期管理起来麻烦(例:componentWillMount,不保证只调用一次)
- 函数组件+ hooks可以实现类组件的功能。
useState
let [value, setValue] = useState(initValue)
//value是当前状态
//setValue是变更状态的函数
//useState就是一个hooks
//initValue就是设置的初始状态
使用useState实现一个简单的计数功能:
function Counter(){
const [number, setNumber] = useState(0);
return (
<>
<p>计数:{number}</p>
<button onClick={()=> setNumber(number+1)}>+</button>
<button onClick={()=> setNumber(number-1)}>-</button>
</>
)
}
代码参考:useState函数组件计数
根据上面的函数组件,number是维护计数的状态 ,setNumber用来触发更新状态。
useState返回的初始值以及该变更方法,每次变更后,还重新渲染了组件:
/*
* 传参:initialState 初始值
返回: 值,以及更新方法[state, setState]
*/
//维护一个备忘状态
let memorizedState;
function useState(initialState) {
memorizedState = memorizedState || initialState;
function setState(newState) {
memorizedState = newState;
render();
}
return [memorizedState, setState];
}
代码参考:useState_easy1
但是呢,上面这个实现的useState,存在问题,就是如果再初始化一个名称,也就是多次调用useState,就会状态混乱。因为变量memorizedState就是七秒钟记忆的鱼,只维护了上次的状态,多个状态就蒙了。
所以 ,我们考虑先用数组维护一下各自的状态,每次通过索引获取对应的,把上面的改一下:
//维护一个备忘状态
let memorizedState = [];
let index = 0; //每次useState初始化时,传入这个索引
function useState(initialState) {
memorizedState[index] = memorizedState[index] || initialState;
let currentIndex = index
function setState(newState) {
memorizedState[currentIndex] = newState;
index = 0; //渲染前要归0
render();
}
return [memorizedState[index++], setState]; //下次要加1
}
代码参考:useState
useEffect
副作用是一个不得不提的钩子,在中文里副作用似乎不是啥好事,但是在hooks中,副作用的功能可是相当强大,它可能替代类组件中的生命周期,如下图:
根据之前的例子我们添加useEffect,实现计数变更,则打印。
useEffect(()=>{
console.log('number1', number);
});
useEffect(()=>{
console.log('number2', number);
}, [number]);
useEffect(()=>{
console.log('number3', number);
}, [number, name]);
参考代码:使用useEffect-1
实现useEffect方法
let lastDep;
//callback:回调函数,deps依赖项
function useEffect(callback, deps){
if(!deps) return callback(); //如果没有依赖项,直接执行
let changed = lastDep?!deps.every((item, idx) => item === lastDep[idex]):true;
if(changed){//如果依赖项变更了,会执行回调
callback();
lastDep = deps;
}
}
参考代码:实现useEffect-1
但是,这个方法有点问题,如果依赖项相同,多使用几次useEffect(()=>{}),便不会触发回调,也就是索引问题。
可以试一试:
useEffect(()=>{
console.log('number2', number);
}, [number]);
useEffect(()=>{
console.log('number2-1', number);
}, [number]);
useEffect正常是两个都会执行并打印,但是上面写的由于共用一个lastDeps变更,所以才只执行了一次。
那我们按照前面useState借助数组的方法来优化一下。
//...
function useEffect(callback, deps){
if(!deps) {
index++;
return callback()
}; //如果没有依赖项,直接执行
let lastDeps = memorizedState[index];
let changed = lastDeps?!deps.every((item, idx) => item === lastDeps[idx]):true;
if(changed){
callback();
lastDeps = deps;
memorizedState[index] = deps;
}
index++;
}
代码参考:useState&&useEffect
useReducer
useReducer是个高级hook,如果了解过redux,那就会发现useReducer跟redux里的reducer超级像。
它也可以进行复杂状态的维护,我们先使用useReducer实现一下上面的计数器功能:
let initalArg = 0;
/*
* 处理并返回新状态
*/
function reducer(state, action){
switch(action.type){
case 'add':return { number: state.number+1};
case 'minus':return { number:state.number-1};
default:
return state;
break;
}
}
/*
* 初始化状态的方法
*/
function init(initalArg){
return { number: initalArg};
}
function Counter(){
let [state, dispatch] = useReducer(reducer, initalArg, init);
return (
<>
<p>计数器:{state.number}</p>
<button onClick={()=> dispatch({type:'add'}) } >+</button>
<button onClick={()=> dispatch({type:'minus'}) }>-</button>
</>
)
}
代码参考:useReducer计数器
逻辑图如下:
那我们就实现一下useReducer:
let memoizedState;
function useReducer(reducer, initialArgs,init){
let initState;
if(typeof init !== 'undefined'){
initState = init(initialArgs);
}else {
initState = initialArgs;
}
memoizedState = memoizedState || initState;
function dispatch(action){
memoizedState = reducer(memoizedState, action);
render(); //重新渲染
}
return [memoizedState, dispatch];
}
代码参考:实现useReducer-1
变更多个状态,比如名称也修改一下,需要在dispatch派发事件传payload对象。
//reducer中追加一个editName的行为
function reducer(state, action){
switch(action.type){
case 'add':return { ...state, number: state.number+1};
case 'minus':return {...state, number:state.number-1};
case 'editName':
let { name } = action.payload;
return {...state, name };
default:
return state;
break;
}
}
//初始化状态时name
/*
* 初始化状态的方法
*/
function init(initalArg){
return { number: initalArg, name: '计数器'};
}
//页面中加dom
<button onClick={()=> dispatch({type:'editName', payload:{
name:'计数器'+ Date.now()
}})}>变更名称:</button>
代码参考:useReducer-2
使用useReducer实现useState:
function useState(initState){
//useReducer参数:reducer, initValue,init(可选)
return useReducer((oldState, newState) =>{
return newState;
},initState);
}
链表实现
先理解一下链表结构:
那我们定义一个对象来表示链接结构:
//第一个节点的next指向下一个元素,hook = hook.next,就可能实现往后移动指针并遍历。
let hook = {
state: null,
next: null,
}
再看一下useState用链表实现的逻辑图:
//链表实现
let firstWorkInProgressHook = {
memoizedState:null,
next:null,
};//第一个钩子
let workInProgressHook = firstWorkInProgressHook; //当前工作中的hook
function useState(initialState){
//判断如果没有就初始化一个,有的话,当前指针向后移。
let currentHook = workInProgressHook.next?workInProgressHook.next:{
memoizedState:null,
next:null,
}
function setState(newState){
currentHook.memoizedState = newState;
workInProgressHook = firstWorkInProgressHook; //重新渲染时,重置work
render(); //重新渲染
}
if(workInProgressHook.next){ //工作中的hook如果存在next
workInProgressHook = workInProgressHook.next; //往后移位。
}else {
workInProgressHook.next = currentHook; //如果当前工作的没有下一个要执行钩子,则给它挂上。
workInProgressHook = currentHook
}
return [currentHook.memoizedState, setState]
}
参考代码:链表实现useState
简单版本的hooks就先学习到这里。